mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-25 09:49:15 +00:00
Merge branch 'bra-port' of ssh.gitgud.io:sin1337/Bondage-College into bra-port
This commit is contained in:
commit
1af19885b9
36 changed files with 669 additions and 278 deletions
BondageClub
Assets/Female3DCG
CSS
Icons
Screens
Character
FriendList
Preference
Inventory/ItemHandheld
Room/Crafting
Scripts
|
@ -6448,7 +6448,8 @@ ItemHoodCreepyIronMaskSelectBase,Creepy Iron Mask main menu:
|
||||||
ItemHoodCreepyIronMaskModuleMode,Mask style
|
ItemHoodCreepyIronMaskModuleMode,Mask style
|
||||||
ItemHoodCreepyIronMaskSelectMode,Mask menu:
|
ItemHoodCreepyIronMaskSelectMode,Mask menu:
|
||||||
ItemHoodCreepyIronMaskOptionm0,Just mask
|
ItemHoodCreepyIronMaskOptionm0,Just mask
|
||||||
ItemHoodCreepyIronMaskOptionm1,Full hood
|
ItemHoodCreepyIronMaskOptionm1,Semi-hood
|
||||||
|
ItemHoodCreepyIronMaskOptionm2,Full hood
|
||||||
ItemHoodCreepyIronMaskModuleBlindfold,Blindfold
|
ItemHoodCreepyIronMaskModuleBlindfold,Blindfold
|
||||||
ItemHoodCreepyIronMaskSelectBlindfold,Metal blindfold option:
|
ItemHoodCreepyIronMaskSelectBlindfold,Metal blindfold option:
|
||||||
ItemHoodCreepyIronMaskOptionb0,None
|
ItemHoodCreepyIronMaskOptionb0,None
|
||||||
|
@ -6462,8 +6463,13 @@ ItemHoodCreepyIronMaskModuleNose,Nose guard
|
||||||
ItemHoodCreepyIronMaskSelectNose,Nose guard option:
|
ItemHoodCreepyIronMaskSelectNose,Nose guard option:
|
||||||
ItemHoodCreepyIronMaskOptionn0,None
|
ItemHoodCreepyIronMaskOptionn0,None
|
||||||
ItemHoodCreepyIronMaskOptionn1,Add nose guard
|
ItemHoodCreepyIronMaskOptionn1,Add nose guard
|
||||||
|
ItemHoodCreepyIronMaskModuleSpeech,Speech
|
||||||
|
ItemHoodCreepyIronMaskSelectSpeech,Speech option:
|
||||||
|
ItemHoodCreepyIronMaskOptionp0,Loose
|
||||||
|
ItemHoodCreepyIronMaskOptionp1,Tight
|
||||||
ItemHoodCreepyIronMaskSetm0,SourceCharacter puts the Creepy Iron Mask on DestinationCharacterName head.
|
ItemHoodCreepyIronMaskSetm0,SourceCharacter puts the Creepy Iron Mask on DestinationCharacterName head.
|
||||||
ItemHoodCreepyIronMaskSetm1,SourceCharacter locks DestinationCharacterName head with the Creepy Iron Hood version.
|
ItemHoodCreepyIronMaskSetm1,SourceCharacter locks DestinationCharacterName head with the Creepy Iron Hood version.
|
||||||
|
ItemHoodCreepyIronMaskSetm2,SourceCharacter locks DestinationCharacterName head with the Creepy Iron Hood version.
|
||||||
ItemHoodCreepyIronMaskSetb0,SourceCharacter removes a metal blindfold from DestinationCharacterName Creepy Iron Mask.
|
ItemHoodCreepyIronMaskSetb0,SourceCharacter removes a metal blindfold from DestinationCharacterName Creepy Iron Mask.
|
||||||
ItemHoodCreepyIronMaskSetb1,SourceCharacter equips a perforated metal blindfold on DestinationCharacterName Creepy Iron Mask.
|
ItemHoodCreepyIronMaskSetb1,SourceCharacter equips a perforated metal blindfold on DestinationCharacterName Creepy Iron Mask.
|
||||||
ItemHoodCreepyIronMaskSetb2,SourceCharacter equips a full metal blindfold on DestinationCharacterName Creepy Iron Mask.
|
ItemHoodCreepyIronMaskSetb2,SourceCharacter equips a full metal blindfold on DestinationCharacterName Creepy Iron Mask.
|
||||||
|
@ -6471,6 +6477,8 @@ ItemHoodCreepyIronMaskSets0,SourceCharacter removes a spiked collar from Destina
|
||||||
ItemHoodCreepyIronMaskSets1,SourceCharacter equips a spiked collar on DestinationCharacterName neck.
|
ItemHoodCreepyIronMaskSets1,SourceCharacter equips a spiked collar on DestinationCharacterName neck.
|
||||||
ItemHoodCreepyIronMaskSetn0,SourceCharacter removes a nose guard from DestinationCharacterName Creepy Iron Mask.
|
ItemHoodCreepyIronMaskSetn0,SourceCharacter removes a nose guard from DestinationCharacterName Creepy Iron Mask.
|
||||||
ItemHoodCreepyIronMaskSetn1,SourceCharacter equips a nose guard on DestinationCharacterName Creepy Iron Mask.
|
ItemHoodCreepyIronMaskSetn1,SourceCharacter equips a nose guard on DestinationCharacterName Creepy Iron Mask.
|
||||||
|
ItemHoodCreepyIronMaskSetp0,SourceCharacter loosens DestinationCharacterName Creepy Iron Mask.
|
||||||
|
ItemHoodCreepyIronMaskSetp1,SourceCharacter tightens DestinationCharacterName Creepy Iron Mask.
|
||||||
ItemMouthHorrorMuzzleSelect,Horror Muzzle style option menu:
|
ItemMouthHorrorMuzzleSelect,Horror Muzzle style option menu:
|
||||||
ItemMouthHorrorMuzzleNone,Plain
|
ItemMouthHorrorMuzzleNone,Plain
|
||||||
ItemMouthHorrorMuzzleRivets,With rivets
|
ItemMouthHorrorMuzzleRivets,With rivets
|
||||||
|
@ -6521,12 +6529,12 @@ ItemHandheldKyosensuType3,Sun
|
||||||
ItemHandheldKyosensuType4,Wave art
|
ItemHandheldKyosensuType4,Wave art
|
||||||
ItemHandheldKyosensuType5,Moon
|
ItemHandheldKyosensuType5,Moon
|
||||||
ItemHandheldKyosensuType6,Flowers
|
ItemHandheldKyosensuType6,Flowers
|
||||||
ItemItemHandheldKyosensuSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Kyo-sensu fan.
|
ItemHandheldKyosensuSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Kyo-sensu fan.
|
||||||
ItemItemHandheldKyosensuSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Kyo-sensu fan.
|
ItemHandheldKyosensuSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Kyo-sensu fan.
|
||||||
ItemItemHandheldKyosensuSetType3,SourceCharacter chooses a sun art for DestinationCharacter Kyo-sensu fan.
|
ItemHandheldKyosensuSetType3,SourceCharacter chooses a sun art for DestinationCharacter Kyo-sensu fan.
|
||||||
ItemItemHandheldKyosensuSetType4,SourceCharacter chooses a wave art for DestinationCharacter Kyo-sensu fan.
|
ItemHandheldKyosensuSetType4,SourceCharacter chooses a wave art for DestinationCharacter Kyo-sensu fan.
|
||||||
ItemItemHandheldKyosensuSetType5,SourceCharacter chooses a moon art for DestinationCharacter Kyo-sensu fan.
|
ItemHandheldKyosensuSetType5,SourceCharacter chooses a moon art for DestinationCharacter Kyo-sensu fan.
|
||||||
ItemItemHandheldKyosensuSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Kyo-sensu fan.
|
ItemHandheldKyosensuSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Kyo-sensu fan.
|
||||||
ItemHandheldUchiwaSelect,Choose your Uchiwa paper art:
|
ItemHandheldUchiwaSelect,Choose your Uchiwa paper art:
|
||||||
ItemHandheldUchiwaType1,Cherry blossoms
|
ItemHandheldUchiwaType1,Cherry blossoms
|
||||||
ItemHandheldUchiwaType2,Lightning bolt
|
ItemHandheldUchiwaType2,Lightning bolt
|
||||||
|
@ -6534,9 +6542,9 @@ ItemHandheldUchiwaType3,Sun
|
||||||
ItemHandheldUchiwaType4,Wave art
|
ItemHandheldUchiwaType4,Wave art
|
||||||
ItemHandheldUchiwaType5,Moon
|
ItemHandheldUchiwaType5,Moon
|
||||||
ItemHandheldUchiwaType6,Flowers
|
ItemHandheldUchiwaType6,Flowers
|
||||||
ItemItemHandheldUchiwaSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Uchiwa fan.
|
ItemHandheldUchiwaSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Uchiwa fan.
|
||||||
ItemItemHandheldUchiwaSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Uchiwa fan.
|
ItemHandheldUchiwaSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Uchiwa fan.
|
||||||
ItemItemHandheldUchiwaSetType3,SourceCharacter chooses a sun art for DestinationCharacter Uchiwa fan.
|
ItemHandheldUchiwaSetType3,SourceCharacter chooses a sun art for DestinationCharacter Uchiwa fan.
|
||||||
ItemItemHandheldUchiwaSetType4,SourceCharacter chooses a wave art for DestinationCharacter Uchiwa fan.
|
ItemHandheldUchiwaSetType4,SourceCharacter chooses a wave art for DestinationCharacter Uchiwa fan.
|
||||||
ItemItemHandheldUchiwaSetType5,SourceCharacter chooses a moon art for DestinationCharacter Uchiwa fan.
|
ItemHandheldUchiwaSetType5,SourceCharacter chooses a moon art for DestinationCharacter Uchiwa fan.
|
||||||
ItemItemHandheldUchiwaSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Uchiwa fan.
|
ItemHandheldUchiwaSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Uchiwa fan.
|
||||||
|
|
Can't render this file because it contains an unexpected character in line 6406 and column 135.
|
|
@ -17386,36 +17386,6 @@ var AssetFemale3DCG = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "AsylumBlindfold",
|
|
||||||
InventoryID: 1223,
|
|
||||||
BuyGroup: "AsylumMuzzle",
|
|
||||||
Top: 25,
|
|
||||||
Value: -1,
|
|
||||||
Difficulty: 3,
|
|
||||||
Time: 10,
|
|
||||||
DefaultColor: ["#BFBFB1", "#95866D", "#A0A0A0", "#877C66"],
|
|
||||||
Extended: true,
|
|
||||||
Random: true,
|
|
||||||
AllowLock: true,
|
|
||||||
AllowTighten: true,
|
|
||||||
DrawLocks: false,
|
|
||||||
Audio: "Buckle",
|
|
||||||
Hide: ["Glasses"],
|
|
||||||
ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ],
|
|
||||||
Layer: [
|
|
||||||
{ Name: "Main", Priority: 35, AllowColorize: true },
|
|
||||||
{
|
|
||||||
Name: "Pad",
|
|
||||||
Priority: 34,
|
|
||||||
CreateLayerTypes: ["typed"],
|
|
||||||
AllowTypes: { typed: 1 },
|
|
||||||
AllowColorize: true,
|
|
||||||
},
|
|
||||||
{ Name: "Metal", Priority: 35, AllowColorize: true },
|
|
||||||
{ Name: "Lock", Priority: 35, LockLayer: true, AllowColorize: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
Color: [
|
Color: [
|
||||||
"Default",
|
"Default",
|
||||||
|
@ -38081,7 +38051,10 @@ var AssetFemale3DCG = [
|
||||||
AllowTighten: true,
|
AllowTighten: true,
|
||||||
DrawLocks: false,
|
DrawLocks: false,
|
||||||
Audio: "Buckle",
|
Audio: "Buckle",
|
||||||
ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ],
|
ExpressionTrigger: [
|
||||||
|
{ Name: "Soft", Group: "Eyebrows", Timer: 15 },
|
||||||
|
{ Name: "Shy", Group: "Eyes", Timer: 15 },
|
||||||
|
],
|
||||||
Layer: [
|
Layer: [
|
||||||
{ Name: "Main", Priority: 35, AllowColorize: true },
|
{ Name: "Main", Priority: 35, AllowColorize: true },
|
||||||
{
|
{
|
||||||
|
@ -41813,7 +41786,10 @@ var AssetFemale3DCG = [
|
||||||
Effect: [E.BlockMouth],
|
Effect: [E.BlockMouth],
|
||||||
Prerequisite: ["GagFlat"],
|
Prerequisite: ["GagFlat"],
|
||||||
Hide: ["ItemNose"],
|
Hide: ["ItemNose"],
|
||||||
ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ],
|
ExpressionTrigger: [
|
||||||
|
{ Name: "Soft", Group: "Eyebrows", Timer: 15 },
|
||||||
|
{ Name: "Shy", Group: "Eyes", Timer: 15 },
|
||||||
|
],
|
||||||
Layer: [
|
Layer: [
|
||||||
{ Name: "Main", Priority: 38, AllowColorize: true },
|
{ Name: "Main", Priority: 38, AllowColorize: true },
|
||||||
{
|
{
|
||||||
|
@ -45775,7 +45751,10 @@ var AssetFemale3DCG = [
|
||||||
Effect: [E.BlockMouth],
|
Effect: [E.BlockMouth],
|
||||||
Prerequisite: ["GagFlat"],
|
Prerequisite: ["GagFlat"],
|
||||||
Hide: ["ItemNose"],
|
Hide: ["ItemNose"],
|
||||||
ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ],
|
ExpressionTrigger: [
|
||||||
|
{ Name: "Soft", Group: "Eyebrows", Timer: 15 },
|
||||||
|
{ Name: "Shy", Group: "Eyes", Timer: 15 },
|
||||||
|
],
|
||||||
Layer: [
|
Layer: [
|
||||||
{ Name: "Main", Priority: 39, AllowColorize: true },
|
{ Name: "Main", Priority: 39, AllowColorize: true },
|
||||||
{
|
{
|
||||||
|
@ -47022,6 +47001,39 @@ var AssetFemale3DCG = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "AsylumBlindfold",
|
||||||
|
InventoryID: 1223,
|
||||||
|
BuyGroup: "AsylumMuzzle",
|
||||||
|
Top: 25,
|
||||||
|
Value: -1,
|
||||||
|
Difficulty: 3,
|
||||||
|
Time: 10,
|
||||||
|
DefaultColor: ["#BFBFB1", "#95866D", "#A0A0A0", "#877C66"],
|
||||||
|
Extended: true,
|
||||||
|
Random: true,
|
||||||
|
AllowLock: true,
|
||||||
|
AllowTighten: true,
|
||||||
|
DrawLocks: false,
|
||||||
|
Audio: "Buckle",
|
||||||
|
Hide: ["Glasses"],
|
||||||
|
ExpressionTrigger: [
|
||||||
|
{ Name: "Soft", Group: "Eyebrows", Timer: 15 },
|
||||||
|
{ Name: "Shy", Group: "Eyes", Timer: 15 },
|
||||||
|
],
|
||||||
|
Layer: [
|
||||||
|
{ Name: "Main", Priority: 35, AllowColorize: true },
|
||||||
|
{
|
||||||
|
Name: "Pad",
|
||||||
|
Priority: 34,
|
||||||
|
CreateLayerTypes: ["typed"],
|
||||||
|
AllowTypes: { typed: 1 },
|
||||||
|
AllowColorize: true,
|
||||||
|
},
|
||||||
|
{ Name: "Metal", Priority: 35, AllowColorize: true },
|
||||||
|
{ Name: "Lock", Priority: 35, LockLayer: true, AllowColorize: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
Color: [
|
Color: [
|
||||||
"Default",
|
"Default",
|
||||||
|
|
|
@ -9067,7 +9067,7 @@ var AssetFemale3DCGExtended = {
|
||||||
// Semi-Hood
|
// Semi-Hood
|
||||||
Property: {
|
Property: {
|
||||||
Effect: [E.BlockMouth],
|
Effect: [E.BlockMouth],
|
||||||
Hide: ["HairFront","HairAccessory1"],
|
Hide: ["HairFront", "HairAccessory1"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -657,6 +657,7 @@ interface AssetDefinitionBase extends AssetCommonPropertiesGroupAsset, AssetComm
|
||||||
AllowHideItem?: string[];
|
AllowHideItem?: string[];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
AllowTypes?: never;
|
AllowTypes?: never;
|
||||||
|
/** A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. */
|
||||||
CreateLayerTypes?: string[];
|
CreateLayerTypes?: string[];
|
||||||
/**
|
/**
|
||||||
* Whether an item can be tightened or not.
|
* Whether an item can be tightened or not.
|
||||||
|
@ -819,7 +820,7 @@ interface AssetLayerDefinition extends AssetCommonPropertiesGroupAssetLayer, Ass
|
||||||
/** Whether the layer is hidden in the Color Picker UI. Defaults to false. */
|
/** Whether the layer is hidden in the Color Picker UI. Defaults to false. */
|
||||||
HideColoring?: boolean;
|
HideColoring?: boolean;
|
||||||
|
|
||||||
/** A record (or a list thereof) with all screen names + option indices that should make the layer visible */
|
/** A record (or a list thereof) with all screen names + option indices, _i.e._ {@link TypeRecord} keys + values, that should make the layer visible */
|
||||||
AllowTypes?: AllowTypes.Definition;
|
AllowTypes?: AllowTypes.Definition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -867,6 +868,11 @@ interface AssetLayerDefinition extends AssetCommonPropertiesGroupAssetLayer, Ass
|
||||||
*/
|
*/
|
||||||
CopyLayerPoseMapping?: string;
|
CopyLayerPoseMapping?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files.
|
||||||
|
*
|
||||||
|
* By default files are expected for _all_ option indices associated with the key(s), unless the valid option set has been narrowed down according to {@link AssetLayerDefinition.AllowTypes}.
|
||||||
|
*/
|
||||||
CreateLayerTypes?: string[];
|
CreateLayerTypes?: string[];
|
||||||
/* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */
|
/* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */
|
||||||
HideForAttribute?: AssetAttribute[];
|
HideForAttribute?: AssetAttribute[];
|
||||||
|
|
Binary file not shown.
Before ![]() (image error) Size: 19 KiB |
Binary file not shown.
Before ![]() (image error) Size: 12 KiB |
|
@ -33,6 +33,7 @@
|
||||||
padding: var(--small-gap);
|
padding: var(--small-gap);
|
||||||
width: 20%;
|
width: 20%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#friend-list-search-input {
|
#friend-list-search-input {
|
||||||
|
@ -61,17 +62,19 @@
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* #region HEADER */
|
/* #region HEADER */
|
||||||
|
|
||||||
#friend-list-header {
|
#friend-list-header {
|
||||||
min-height: var(--row-height);
|
user-select: none;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#friend-list-header .friend-list-link {
|
#friend-list-header .friend-list-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#friend-list-header .friend-list-row:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
#friend-list-header-hr {
|
#friend-list-header-hr {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
|
@ -105,6 +108,60 @@
|
||||||
#friend-list-beep-dialog:not([data-received]) .fl-beep-sent-content {
|
#friend-list-beep-dialog:not([data-received]) .fl-beep-sent-content {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ChatRoomType {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.15em;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-list-icon-container {
|
||||||
|
position: relative;
|
||||||
|
height: var(--row-height);
|
||||||
|
width: var(--row-height);
|
||||||
|
max-width: 86px;
|
||||||
|
max-height: 86px;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-list-icon-container > .button-tooltip {
|
||||||
|
--tooltip-gap: 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-list-icon {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
@supports selector(:has(*)) {
|
||||||
|
.friend-list-icon-container:hover:not(:has(.button-tooltip:hover)) > .button-tooltip {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports not selector(:has(*)) {
|
||||||
|
.friend-list-icon-container:hover > .button-tooltip {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-list-icon-small {
|
||||||
|
pointer-events: none;
|
||||||
|
height: var(--row-height);
|
||||||
|
width: var(--row-height);
|
||||||
|
max-width: 50px;
|
||||||
|
max-height: 50px;
|
||||||
|
margin-inline: 0.15em;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* #region FRIENDLIST */
|
/* #region FRIENDLIST */
|
||||||
|
@ -124,17 +181,27 @@
|
||||||
width: calc(100% - var(--button-size));
|
width: calc(100% - var(--button-size));
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-list-row {
|
#friend-list-table th {
|
||||||
color: var(--text-color);
|
font-weight: normal;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-list-row * {
|
.friend-list-row {
|
||||||
|
color: var(--text-color);
|
||||||
|
min-height: var(--row-height);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
padding: unset;
|
||||||
margin: var(--small-gap) 0;
|
margin: var(--small-gap) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.friend-list-row > * {
|
||||||
|
vertical-align: middle;
|
||||||
|
text-justify: center;
|
||||||
|
padding: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.friend-list-row:hover {
|
.friend-list-row:hover {
|
||||||
color: yellow;
|
color: yellow;
|
||||||
}
|
}
|
||||||
|
@ -147,10 +214,6 @@
|
||||||
white-space: preserve;
|
white-space: preserve;
|
||||||
}
|
}
|
||||||
|
|
||||||
#friend-list .friend-list-column {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#friend-list-member-number,
|
#friend-list-member-number,
|
||||||
.MemberNumber {
|
.MemberNumber {
|
||||||
width: 10%;
|
width: 10%;
|
||||||
|
@ -168,8 +231,8 @@
|
||||||
gap: var(--small-gap);
|
gap: var(--small-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.RelationType img {
|
.RelationType {
|
||||||
height: min(5dvh, 2.5dvw);
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.friend-list-link {
|
.friend-list-link {
|
||||||
|
|
|
@ -223,41 +223,58 @@ select:invalid:not(:disabled):not(:read-only) {
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
.checkbox {
|
||||||
|
/* Default background color */
|
||||||
|
--checkbox-color: white;
|
||||||
|
/* Background color for disabled buttons */
|
||||||
|
--disabled-color: grey;
|
||||||
|
/* Background color for hovered and/or active buttons */
|
||||||
|
--hover-color: cyan;
|
||||||
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: #fff;
|
background-color: var(--checkbox-color);
|
||||||
margin: 0;
|
position: relative;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: black;
|
color: black;
|
||||||
width: min(6vh, 3vw);
|
width: min(6vh, 3vw);
|
||||||
height: min(6vh, 3vw);
|
height: min(6vh, 3vw);
|
||||||
border: min(0.3vh, 0.15vw) solid black;
|
border: min(0.2vh, 0.1vw) solid black;
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"]:hover {
|
.checkbox:hover {
|
||||||
background-color: cyan;
|
background-color: var(--hover-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"]:disabled {
|
.checkbox:disabled {
|
||||||
background-color: lightgray;
|
background-color: var(--disabled-color);
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"]::before {
|
.checkbox::before {
|
||||||
content: "";
|
content: "";
|
||||||
width: min(4.6vh, 2.3vw);
|
position: absolute;
|
||||||
height: min(4.6vh, 2.3vw);
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||||
transform: scale(0);
|
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
width: 90%;
|
||||||
|
height: 90%;
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"]:checked::before {
|
.checkbox:checked::before {
|
||||||
transform: scale(1);
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (height: 100dvh) {
|
||||||
|
.checkbox {
|
||||||
|
width: min(6dvh, 3dvw);
|
||||||
|
height: min(6dvh, 3dvw);
|
||||||
|
border-width: min(0.2dvh, 0.1dvw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dropdown */
|
/* Dropdown */
|
||||||
|
|
BIN
BondageClub/Icons/FemaleInverted.png
Normal file
BIN
BondageClub/Icons/FemaleInverted.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 3.5 KiB |
BIN
BondageClub/Icons/GenderInvert.png
Normal file
BIN
BondageClub/Icons/GenderInvert.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2 KiB |
BIN
BondageClub/Icons/MaleInverted.png
Normal file
BIN
BondageClub/Icons/MaleInverted.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 3.6 KiB |
BIN
BondageClub/Icons/PrivateInvert.png
Normal file
BIN
BondageClub/Icons/PrivateInvert.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2.1 KiB |
|
@ -1,6 +1,6 @@
|
||||||
type FriendListModes = FriendListMode[];
|
type FriendListModes = FriendListMode[];
|
||||||
type FriendListMode = "OnlineFriends" | "Beeps" | "AllFriends";
|
type FriendListMode = "OnlineFriends" | "Beeps" | "AllFriends";
|
||||||
type FriendListSortingMode = 'None' | 'MemberName' | 'MemberNickname' | 'MemberNumber' | 'ChatRoomName' | 'RelationType';
|
type FriendListSortingMode = 'None' | 'MemberName' | 'MemberNickname' | 'MemberNumber' | 'ChatRoomName' | 'RelationType' | 'ChatRoomType';
|
||||||
type FriendListSortingDirection = 'Asc' | 'Desc';
|
type FriendListSortingDirection = 'Asc' | 'Desc';
|
||||||
|
|
||||||
type FriendListReturn<T extends ModuleType> = { Screen: ModuleScreens[T], Module: T, IsInChatRoom?: boolean, hasScrolledChat?: boolean };
|
type FriendListReturn<T extends ModuleType> = { Screen: ModuleScreens[T], Module: T, IsInChatRoom?: boolean, hasScrolledChat?: boolean };
|
||||||
|
@ -19,6 +19,7 @@ type FriendRawRoom = {
|
||||||
name?: string;
|
name?: string;
|
||||||
caption: string;
|
caption: string;
|
||||||
canSearchRoom: boolean;
|
canSearchRoom: boolean;
|
||||||
|
types: FriendListIcon[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type FriendRawBeep = {
|
type FriendRawBeep = {
|
||||||
|
@ -27,3 +28,12 @@ type FriendRawBeep = {
|
||||||
hasMessage?: boolean;
|
hasMessage?: boolean;
|
||||||
canBeep?: boolean;
|
canBeep?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface FriendListIcon {
|
||||||
|
/** The {@link HTMLImageElement.src} of the icon */
|
||||||
|
src: string;
|
||||||
|
/** The `Character/FriendList` {@link TextGet} key of the icon's tooltip */
|
||||||
|
tooltipKey: string;
|
||||||
|
/** A string to-be used for sorting the icon-containing column cells */
|
||||||
|
sortKey: string;
|
||||||
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ function FriendListLoad() {
|
||||||
tag: 'input',
|
tag: 'input',
|
||||||
attributes: {
|
attributes: {
|
||||||
id: FriendListIDs.searchInput,
|
id: FriendListIDs.searchInput,
|
||||||
type: 'text',
|
type: 'search',
|
||||||
maxLength: 100,
|
maxLength: 100,
|
||||||
},
|
},
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
|
@ -198,71 +198,87 @@ function FriendListLoad() {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
ElementCreate({
|
ElementCreate({
|
||||||
tag: 'div',
|
tag: 'table',
|
||||||
attributes: {
|
attributes: {
|
||||||
id: FriendListIDs.friendListTable
|
id: FriendListIDs.friendListTable,
|
||||||
|
"aria-labelledby": FriendListIDs.modeTitle,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
tag: 'div',
|
tag: 'thead',
|
||||||
attributes: {
|
attributes: {
|
||||||
id: FriendListIDs.header
|
id: FriendListIDs.header
|
||||||
},
|
},
|
||||||
classList: ['friend-list-row'],
|
|
||||||
children: [
|
children: [
|
||||||
ElementButton.Create(
|
|
||||||
"friend-list-member-name",
|
|
||||||
() => FriendListChangeSortingMode("MemberName"),
|
|
||||||
{ noStyling: true },
|
|
||||||
{ button: {
|
|
||||||
classList: ['friend-list-column', 'friend-list-link'],
|
|
||||||
}},
|
|
||||||
),
|
|
||||||
ElementButton.Create(
|
|
||||||
"friend-list-member-number",
|
|
||||||
() => FriendListChangeSortingMode("MemberNumber"),
|
|
||||||
{ noStyling: true },
|
|
||||||
{ button: {
|
|
||||||
classList: ['friend-list-column', 'friend-list-link'],
|
|
||||||
}},
|
|
||||||
),
|
|
||||||
ElementButton.Create(
|
|
||||||
"friend-list-chat-room-name",
|
|
||||||
() => FriendListChangeSortingMode("ChatRoomName"),
|
|
||||||
{ noStyling: true },
|
|
||||||
{ button: {
|
|
||||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'],
|
|
||||||
}},
|
|
||||||
),
|
|
||||||
ElementButton.Create(
|
|
||||||
"friend-list-relation-type",
|
|
||||||
() => FriendListChangeSortingMode("RelationType"),
|
|
||||||
{ noStyling: true },
|
|
||||||
{ button: {
|
|
||||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
|
|
||||||
}},
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
tag: "span",
|
tag: "tr",
|
||||||
classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'],
|
classList: ["friend-list-row"],
|
||||||
children: [
|
children: [
|
||||||
TextGet("ActionFriends")
|
ElementButton.Create(
|
||||||
|
"friend-list-member-name",
|
||||||
|
() => FriendListChangeSortingMode("MemberName"),
|
||||||
|
{ noStyling: true },
|
||||||
|
{ button: {
|
||||||
|
classList: ['friend-list-column', 'friend-list-link'],
|
||||||
|
attributes: { role: "columnheader" },
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
ElementButton.Create(
|
||||||
|
"friend-list-member-number",
|
||||||
|
() => FriendListChangeSortingMode("MemberNumber"),
|
||||||
|
{ noStyling: true },
|
||||||
|
{ button: {
|
||||||
|
classList: ['friend-list-column', 'friend-list-link'],
|
||||||
|
attributes: { role: "columnheader" },
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
ElementButton.Create(
|
||||||
|
"friend-list-chat-room-type",
|
||||||
|
() => FriendListChangeSortingMode("ChatRoomType"),
|
||||||
|
{ noStyling: true },
|
||||||
|
{ button: {
|
||||||
|
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'],
|
||||||
|
attributes: { role: "columnheader" },
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
ElementButton.Create(
|
||||||
|
"friend-list-chat-room-name",
|
||||||
|
() => FriendListChangeSortingMode("ChatRoomName"),
|
||||||
|
{ noStyling: true },
|
||||||
|
{ button: {
|
||||||
|
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'],
|
||||||
|
attributes: { role: "columnheader" },
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
ElementButton.Create(
|
||||||
|
"friend-list-relation-type",
|
||||||
|
() => FriendListChangeSortingMode("RelationType"),
|
||||||
|
{ noStyling: true },
|
||||||
|
{ button: {
|
||||||
|
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
|
||||||
|
attributes: { role: "columnheader" },
|
||||||
|
}},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
tag: "th",
|
||||||
|
classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'],
|
||||||
|
attributes: { scope: "col" },
|
||||||
|
children: [TextGet("ActionFriends")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "th",
|
||||||
|
classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'],
|
||||||
|
attributes: { scope: "col" },
|
||||||
|
children: [TextGet("ActionRead")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "th",
|
||||||
|
classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'],
|
||||||
|
attributes: { scope: "col" },
|
||||||
|
children: [TextGet("ActionDelete")],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
tag: "span",
|
|
||||||
classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'],
|
|
||||||
children: [
|
|
||||||
TextGet("ActionRead")
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "span",
|
|
||||||
classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'],
|
|
||||||
children: [
|
|
||||||
TextGet("ActionDelete")
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -272,7 +288,7 @@ function FriendListLoad() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'div',
|
tag: 'tbody',
|
||||||
classList: ["scroll-box"],
|
classList: ["scroll-box"],
|
||||||
attributes: {
|
attributes: {
|
||||||
id: FriendListIDs.friendList
|
id: FriendListIDs.friendList
|
||||||
|
@ -504,7 +520,7 @@ function FriendListBeepMenuSend() {
|
||||||
ChatRoomName: FriendListBeepShowRoom ? ChatRoomData?.Name : undefined,
|
ChatRoomName: FriendListBeepShowRoom ? ChatRoomData?.Name : undefined,
|
||||||
ChatRoomSpace: FriendListBeepShowRoom ? ChatRoomData?.Space : undefined,
|
ChatRoomSpace: FriendListBeepShowRoom ? ChatRoomData?.Space : undefined,
|
||||||
Sent: true,
|
Sent: true,
|
||||||
Private: false,
|
Private: FriendListBeepShowRoom ? !ChatRoomData?.Visibility.includes("All") : undefined,
|
||||||
Time: new Date(),
|
Time: new Date(),
|
||||||
Message: msg || undefined
|
Message: msg || undefined
|
||||||
});
|
});
|
||||||
|
@ -537,6 +553,15 @@ function FriendListChatSearch(room) {
|
||||||
ElementValue("InputSearch", ChatSearchMuffle(room));
|
ElementValue("InputSearch", ChatSearchMuffle(room));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @satisfies {{ [key in (ServerChatRoomSpace | "Private")]: FriendListIcon }} */
|
||||||
|
const FriendListIconMapping = {
|
||||||
|
"": { src: "./Icons/FemaleInvert.png", tooltipKey: "TypeFemale", sortKey: "F " },
|
||||||
|
M: { src: "./Icons/MaleInvert.png", tooltipKey: "TypeMale", sortKey: "M " },
|
||||||
|
X: { src: "./Icons/GenderInvert.png", tooltipKey: "TypeMixed", sortKey: "FM" },
|
||||||
|
Asylum: { src: "./Icons/Asylum.png", tooltipKey: "TypeAsylum", sortKey: "A " },
|
||||||
|
Private: { src: "./Icons/PrivateInvert.png", tooltipKey: "TypePrivate", sortKey: "P" },
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the friend list data into the HTML div element.
|
* Loads the friend list data into the HTML div element.
|
||||||
* @param {ServerFriendInfo[]} data - An array of data, we receive from the server
|
* @param {ServerFriendInfo[]} data - An array of data, we receive from the server
|
||||||
|
@ -559,7 +584,6 @@ function FriendListLoadFriendList(data) {
|
||||||
const BeepCaption = InterfaceTextGet("Beep");
|
const BeepCaption = InterfaceTextGet("Beep");
|
||||||
const DeleteCaption = InterfaceTextGet("Delete");
|
const DeleteCaption = InterfaceTextGet("Delete");
|
||||||
const ConfirmDeleteCaption = InterfaceTextGet("ConfirmDelete");
|
const ConfirmDeleteCaption = InterfaceTextGet("ConfirmDelete");
|
||||||
const PrivateRoomCaption = InterfaceTextGet("PrivateRoom");
|
|
||||||
const SentCaption = InterfaceTextGet("SentBeep");
|
const SentCaption = InterfaceTextGet("SentBeep");
|
||||||
const ReceivedCaption = InterfaceTextGet("ReceivedBeep");
|
const ReceivedCaption = InterfaceTextGet("ReceivedBeep");
|
||||||
const MailCaption = InterfaceTextGet("BeepWithMail");
|
const MailCaption = InterfaceTextGet("BeepWithMail");
|
||||||
|
@ -601,15 +625,28 @@ function FriendListLoadFriendList(data) {
|
||||||
});
|
});
|
||||||
if (infoChanged) ServerPlayerRelationsSync();
|
if (infoChanged) ServerPlayerRelationsSync();
|
||||||
|
|
||||||
|
/** @satisfies {Record<string, FriendListSortingMode>} */
|
||||||
const columnHeaders = {
|
const columnHeaders = {
|
||||||
"friend-list-member-name": `${TextGet("MemberName")} ${FriendListSortingMode === "MemberName" ? sortingSymbol : "↕"}`,
|
"friend-list-member-name": "MemberName",
|
||||||
"friend-list-member-number": `${TextGet("MemberNumber")} ${FriendListSortingMode === "MemberNumber" ? sortingSymbol : "↕"}`,
|
"friend-list-member-number": "MemberNumber",
|
||||||
"friend-list-chat-room-name": `${TextGet("ChatRoomName")} ${FriendListSortingMode === "ChatRoomName" ? sortingSymbol : "↕"}`,
|
"friend-list-chat-room-name": "ChatRoomName",
|
||||||
"friend-list-relation-type": `${TextGet("FriendType")} ${FriendListSortingMode === "RelationType" ? sortingSymbol : "↕"}`,
|
"friend-list-chat-room-type": "ChatRoomType",
|
||||||
|
"friend-list-relation-type": "RelationType",
|
||||||
};
|
};
|
||||||
CommonEntries(columnHeaders).forEach(([id, textContent]) => {
|
CommonEntries(columnHeaders).forEach(([id, modeName]) => {
|
||||||
const elem = document.getElementById(id);
|
const elem = document.getElementById(id);
|
||||||
elem.textContent = textContent;
|
const elemSortingSymbol = FriendListSortingMode === modeName ? sortingSymbol : "↕";
|
||||||
|
elem.textContent = `${TextGet(modeName)} ${elemSortingSymbol}`;
|
||||||
|
switch (elemSortingSymbol) {
|
||||||
|
case "↑":
|
||||||
|
elem.setAttribute("aria-sort", "ascending");
|
||||||
|
break;
|
||||||
|
case "↓":
|
||||||
|
elem.setAttribute("aria-sort", "descending");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
elem.setAttribute("aria-sort", "none");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** @type {FriendRawData[]} */
|
/** @type {FriendRawData[]} */
|
||||||
|
@ -621,25 +658,20 @@ function FriendListLoadFriendList(data) {
|
||||||
const originalChatRoomName = friend.ChatRoomName || '';
|
const originalChatRoomName = friend.ChatRoomName || '';
|
||||||
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`);
|
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`);
|
||||||
const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined);
|
const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined);
|
||||||
let caption = '';
|
|
||||||
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (friend.ChatRoomSpace || '');
|
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (friend.ChatRoomSpace || '');
|
||||||
const canBeep = true;
|
const canBeep = true;
|
||||||
|
|
||||||
const rawCaption = [];
|
|
||||||
if (chatRoomSpaceCaption && chatRoomName) rawCaption.push(`<i>${chatRoomSpaceCaption}</i>`);
|
|
||||||
if (friend.Private) rawCaption.push(PrivateRoomCaption);
|
|
||||||
if (chatRoomName) rawCaption.push(chatRoomName);
|
|
||||||
if (rawCaption.length === 0) rawCaption.push('-');
|
|
||||||
|
|
||||||
caption = rawCaption.join(' - ');
|
|
||||||
|
|
||||||
friendRawData.push({
|
friendRawData.push({
|
||||||
memberName: friend.MemberName,
|
memberName: friend.MemberName,
|
||||||
memberNumber: friend.MemberNumber,
|
memberNumber: friend.MemberNumber,
|
||||||
chatRoom: {
|
chatRoom: {
|
||||||
name: originalChatRoomName,
|
name: originalChatRoomName,
|
||||||
caption: caption,
|
caption: chatRoomName || "-",
|
||||||
canSearchRoom: canSearchRoom,
|
canSearchRoom: canSearchRoom,
|
||||||
|
types: [
|
||||||
|
chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[friend.ChatRoomSpace ?? ""] : null,
|
||||||
|
friend.Private ? FriendListIconMapping.Private : null,
|
||||||
|
].filter(Boolean),
|
||||||
},
|
},
|
||||||
beep: {
|
beep: {
|
||||||
canBeep: canBeep,
|
canBeep: canBeep,
|
||||||
|
@ -653,18 +685,9 @@ function FriendListLoadFriendList(data) {
|
||||||
const beepData = FriendListBeepLog[i];
|
const beepData = FriendListBeepLog[i];
|
||||||
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`);
|
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`);
|
||||||
const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined);
|
const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined);
|
||||||
let chatRoomCaption = '';
|
|
||||||
let beepCaption = '';
|
let beepCaption = '';
|
||||||
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (beepData.ChatRoomSpace || '');
|
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (beepData.ChatRoomSpace || '');
|
||||||
|
|
||||||
const rawRoomCaption = [];
|
|
||||||
if (chatRoomSpaceCaption && chatRoomName) rawRoomCaption.push(`<i>${chatRoomSpaceCaption}</i>`);
|
|
||||||
if (beepData.Private) rawRoomCaption.push(PrivateRoomCaption);
|
|
||||||
if (chatRoomName) rawRoomCaption.push(chatRoomName);
|
|
||||||
if (rawRoomCaption.length === 0) rawRoomCaption.push('-');
|
|
||||||
|
|
||||||
chatRoomCaption = rawRoomCaption.join(' - ');
|
|
||||||
|
|
||||||
const rawBeepCaption = [];
|
const rawBeepCaption = [];
|
||||||
if (beepData.Sent) {
|
if (beepData.Sent) {
|
||||||
rawBeepCaption.push(SentCaption);
|
rawBeepCaption.push(SentCaption);
|
||||||
|
@ -683,8 +706,12 @@ function FriendListLoadFriendList(data) {
|
||||||
memberNumber: beepData.MemberNumber,
|
memberNumber: beepData.MemberNumber,
|
||||||
chatRoom: {
|
chatRoom: {
|
||||||
name: beepData.ChatRoomName,
|
name: beepData.ChatRoomName,
|
||||||
caption: chatRoomCaption,
|
caption: chatRoomName || "-",
|
||||||
canSearchRoom: canSearchRoom,
|
canSearchRoom: canSearchRoom,
|
||||||
|
types: [
|
||||||
|
chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[beepData.ChatRoomSpace ?? ""] : null,
|
||||||
|
beepData.Private ? FriendListIconMapping.Private : null,
|
||||||
|
].filter(Boolean),
|
||||||
},
|
},
|
||||||
beep: {
|
beep: {
|
||||||
beepIndex: i,
|
beepIndex: i,
|
||||||
|
@ -718,18 +745,18 @@ function FriendListLoadFriendList(data) {
|
||||||
|
|
||||||
friendRawData.forEach(friend => {
|
friendRawData.forEach(friend => {
|
||||||
const row = ElementCreate({
|
const row = ElementCreate({
|
||||||
tag: "div",
|
tag: "tr",
|
||||||
classList: ['friend-list-row'],
|
classList: ['friend-list-row'],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
tag: "span",
|
tag: "td",
|
||||||
classList: ['friend-list-column', 'MemberName'],
|
classList: ['friend-list-column', 'MemberName'],
|
||||||
children: [
|
children: [
|
||||||
friend.memberName
|
friend.memberName
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: "span",
|
tag: "td",
|
||||||
classList: ['friend-list-column', 'MemberNumber'],
|
classList: ['friend-list-column', 'MemberNumber'],
|
||||||
children: [
|
children: [
|
||||||
friend.memberNumber.toString()
|
friend.memberNumber.toString()
|
||||||
|
@ -740,20 +767,96 @@ function FriendListLoadFriendList(data) {
|
||||||
|
|
||||||
if (friend.chatRoom) {
|
if (friend.chatRoom) {
|
||||||
if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) {
|
if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) {
|
||||||
row.appendChild(ElementCreate({
|
// Sorting is performed via each cell's `textContent`,
|
||||||
tag: "span",
|
// so explicitly prepend an invisible node with some sorting key
|
||||||
classList: ['friend-list-column', 'ChatRoomName'],
|
let totalSortKey = "";
|
||||||
innerHTML: friend.chatRoom.caption,
|
const imgContainer = ElementCreate({
|
||||||
}));
|
tag: "td",
|
||||||
|
classList: ['friend-list-column', 'ChatRoomType'],
|
||||||
|
children: [
|
||||||
|
{ tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] },
|
||||||
|
...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => {
|
||||||
|
totalSortKey += sortKey;
|
||||||
|
return {
|
||||||
|
tag: /** @type {const} */("div"),
|
||||||
|
classList: ["friend-list-icon-container"],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: /** @type {const} */("img"),
|
||||||
|
attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) },
|
||||||
|
classList: ["friend-list-icon"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: /** @type {const} */("div"),
|
||||||
|
attributes: { role: "tooltip", "aria-hidden": "true" },
|
||||||
|
children: [TextGet(tooltipKey)],
|
||||||
|
classList: ["button-tooltip", "button-tooltip-right"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
imgContainer.children[0].textContent = totalSortKey + " ";
|
||||||
|
if (imgContainer.children.length === 1) {
|
||||||
|
imgContainer.append("-");
|
||||||
|
}
|
||||||
|
row.append(
|
||||||
|
imgContainer,
|
||||||
|
ElementCreate({
|
||||||
|
tag: "td",
|
||||||
|
classList: ['friend-list-column', 'ChatRoomName'],
|
||||||
|
children: [friend.chatRoom.caption],
|
||||||
|
style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else if (friend.chatRoom.canSearchRoom) {
|
} else if (friend.chatRoom.canSearchRoom) {
|
||||||
row.appendChild(ElementCreate({
|
// Sorting is performed via each cell's `textContent`,
|
||||||
tag: "button",
|
// so explicitly prepend an invisible node with some sorting key
|
||||||
classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'],
|
let totalSortKey = "";
|
||||||
innerHTML: friend.chatRoom.caption,
|
const imgContainer = ElementCreate({
|
||||||
eventListeners: {
|
tag: "td",
|
||||||
click: () => FriendListChatSearch(friend.chatRoom.name),
|
classList: ['friend-list-column', 'ChatRoomType'],
|
||||||
},
|
children: [
|
||||||
}));
|
{ tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] },
|
||||||
|
...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => {
|
||||||
|
totalSortKey += sortKey;
|
||||||
|
return {
|
||||||
|
tag: /** @type {const} */("div"),
|
||||||
|
classList: ["friend-list-icon-container"],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
tag: /** @type {const} */("img"),
|
||||||
|
attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) },
|
||||||
|
classList: ["friend-list-icon"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: /** @type {const} */("div"),
|
||||||
|
attributes: { role: "tooltip", "aria-hidden": "true" },
|
||||||
|
children: [TextGet(tooltipKey)],
|
||||||
|
classList: ["button-tooltip", "button-tooltip-right"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
imgContainer.children[0].textContent = totalSortKey + " ";
|
||||||
|
if (imgContainer.children.length === 1) {
|
||||||
|
imgContainer.append("-");
|
||||||
|
}
|
||||||
|
row.append(
|
||||||
|
imgContainer,
|
||||||
|
ElementCreate({
|
||||||
|
tag: "td",
|
||||||
|
classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'],
|
||||||
|
innerHTML: friend.chatRoom.caption,
|
||||||
|
style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
|
||||||
|
eventListeners: {
|
||||||
|
click: () => FriendListChatSearch(friend.chatRoom.name),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -767,6 +870,7 @@ function FriendListLoadFriendList(data) {
|
||||||
{ button: {
|
{ button: {
|
||||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'],
|
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'],
|
||||||
children: [friend.beep.caption],
|
children: [friend.beep.caption],
|
||||||
|
attributes: { role: "cell" },
|
||||||
}},
|
}},
|
||||||
),
|
),
|
||||||
ElementButton.Create(
|
ElementButton.Create(
|
||||||
|
@ -776,12 +880,13 @@ function FriendListLoadFriendList(data) {
|
||||||
{ button: {
|
{ button: {
|
||||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-beeps-content'],
|
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-beeps-content'],
|
||||||
children: [friend.beep.caption],
|
children: [friend.beep.caption],
|
||||||
|
attributes: { role: "cell" },
|
||||||
}},
|
}},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
row.appendChild(ElementCreate({
|
row.appendChild(ElementCreate({
|
||||||
tag: "span",
|
tag: "td",
|
||||||
classList: ['friend-list-column'],
|
classList: ['friend-list-column'],
|
||||||
children: [
|
children: [
|
||||||
friend.beep.caption
|
friend.beep.caption
|
||||||
|
@ -792,14 +897,18 @@ function FriendListLoadFriendList(data) {
|
||||||
|
|
||||||
if (friend.relationType) {
|
if (friend.relationType) {
|
||||||
row.appendChild(ElementCreate({
|
row.appendChild(ElementCreate({
|
||||||
tag: "span",
|
tag: "td",
|
||||||
classList: ['friend-list-column', 'RelationType', 'mode-specific-content', 'fl-all-friends-content'],
|
classList: ['friend-list-column', 'RelationType', 'mode-specific-content', 'fl-all-friends-content'],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
tag: 'img',
|
tag: "img",
|
||||||
attributes: {
|
attributes: {
|
||||||
src: relationTypeIcons[friend.relationType],
|
src: relationTypeIcons[friend.relationType],
|
||||||
}
|
decoding: "async",
|
||||||
|
loading: "lazy",
|
||||||
|
"aria-hidden": "true",
|
||||||
|
},
|
||||||
|
classList: ["friend-list-icon-small"],
|
||||||
},
|
},
|
||||||
FriendTypeCaption[friend.relationType]
|
FriendTypeCaption[friend.relationType]
|
||||||
],
|
],
|
||||||
|
@ -813,7 +922,7 @@ function FriendListLoadFriendList(data) {
|
||||||
{ button: {
|
{ button: {
|
||||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
|
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
|
||||||
children: [FriendListConfirmDelete.includes(friend.memberNumber) ? ConfirmDeleteCaption : DeleteCaption],
|
children: [FriendListConfirmDelete.includes(friend.memberNumber) ? ConfirmDeleteCaption : DeleteCaption],
|
||||||
attributes: { disabled: !friend.canDelete },
|
attributes: { disabled: !friend.canDelete, role: "cell" },
|
||||||
}}
|
}}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ MemberName,Name
|
||||||
MemberNickname,Nickname
|
MemberNickname,Nickname
|
||||||
MemberNumber,Member number
|
MemberNumber,Member number
|
||||||
ChatRoomName,Chat room
|
ChatRoomName,Chat room
|
||||||
FriendType,Relation type
|
ChatRoomType,Room type
|
||||||
|
RelationType,Relation type
|
||||||
ActionFriends,Send a Beep
|
ActionFriends,Send a Beep
|
||||||
ActionRead,Read a Beep
|
ActionRead,Read a Beep
|
||||||
ActionDelete,Delete a Friend
|
ActionDelete,Delete a Friend
|
||||||
|
@ -21,3 +22,8 @@ TypeOwner,Owner
|
||||||
TypeLover,Lover
|
TypeLover,Lover
|
||||||
TypeSubmissive,Submissive
|
TypeSubmissive,Submissive
|
||||||
TypeFriend,Friend
|
TypeFriend,Friend
|
||||||
|
TypeFemale,Femaly-only room
|
||||||
|
TypeMale,Male-only room
|
||||||
|
TypeMixed,Mixed male/female room
|
||||||
|
TypeAsylum,Asylum room
|
||||||
|
TypePrivate,Private room
|
||||||
|
|
|
|
@ -276,8 +276,11 @@ function PreferenceDecrementArousalFactor(factor) {
|
||||||
* Exits the preference screen
|
* Exits the preference screen
|
||||||
*/
|
*/
|
||||||
function PreferenceSubscreenArousalExit() {
|
function PreferenceSubscreenArousalExit() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PreferenceSubscreenArousalUnload() {
|
||||||
Player.FocusGroup = null;
|
Player.FocusGroup = null;
|
||||||
CharacterAppearanceForceUpCharacter = -1;
|
CharacterAppearanceForceUpCharacter = -1;
|
||||||
CharacterLoadCanvas(Player);
|
CharacterLoadCanvas(Player);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,10 @@ function PreferenceSubscreenAudioClick() {
|
||||||
* Exits the preference screen.
|
* Exits the preference screen.
|
||||||
*/
|
*/
|
||||||
function PreferenceSubscreenAudioExit() {
|
function PreferenceSubscreenAudioExit() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PreferenceSubscreenAudioUnload() {
|
||||||
// If audio has been disabled for notifications, disable each individual notification audio setting
|
// If audio has been disabled for notifications, disable each individual notification audio setting
|
||||||
if (!Player.AudioSettings.Notifications) {
|
if (!Player.AudioSettings.Notifications) {
|
||||||
for (const setting in Player.NotificationSettings) {
|
for (const setting in Player.NotificationSettings) {
|
||||||
|
@ -85,5 +89,4 @@ function PreferenceSubscreenAudioExit() {
|
||||||
if (typeof audio === 'number' && audio > 0) Player.NotificationSettings[setting].Audio = NotificationAudioType.NONE;
|
if (typeof audio === 'number' && audio > 0) Player.NotificationSettings[setting].Audio = NotificationAudioType.NONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,10 @@ function PreferenceSubscreenCensoredWordsClick() {
|
||||||
* Exits the preference screen
|
* Exits the preference screen
|
||||||
*/
|
*/
|
||||||
function PreferenceSubscreenCensoredWordsExit() {
|
function PreferenceSubscreenCensoredWordsExit() {
|
||||||
ElementRemove("InputWord");
|
|
||||||
Player.ChatSettings.CensoredWordsList = PreferenceCensoredWordsList.join("|");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PreferenceSubscreenCensoredWordsUnload() {
|
||||||
|
ElementRemove("InputWord");
|
||||||
|
Player.ChatSettings.CensoredWordsList = PreferenceCensoredWordsList.join("|");
|
||||||
|
}
|
||||||
|
|
|
@ -82,6 +82,9 @@ function PreferenceSubscreenControllerClick() {
|
||||||
* Exits the preference screen
|
* Exits the preference screen
|
||||||
*/
|
*/
|
||||||
function PreferenceSubscreenControllerExit() {
|
function PreferenceSubscreenControllerExit() {
|
||||||
ControllerStopCalibration(true);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PreferenceSubscreenControllerUnload() {
|
||||||
|
ControllerStopCalibration(true);
|
||||||
|
}
|
||||||
|
|
|
@ -68,17 +68,12 @@ function PreferenceSubscreenExtensionsClick() {
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function PreferenceSubscreenExtensionsUnload() {
|
|
||||||
PreferenceExtensionsCurrent?.unload?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
function PreferenceSubscreenExtensionsExit() {
|
function PreferenceSubscreenExtensionsExit() {
|
||||||
if (PreferenceExtensionsCurrent) {
|
if (PreferenceExtensionsCurrent) {
|
||||||
if (PreferenceExtensionsCurrent.exit() ?? true) {
|
const validExit = PreferenceExtensionsCurrent.exit();
|
||||||
PreferenceSubscreenExtensionsClear();
|
if (validExit === false) return false;
|
||||||
return false;
|
PreferenceSubscreenExtensionsClear();
|
||||||
}
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -95,3 +90,13 @@ function PreferenceSubscreenExtensionsClear() {
|
||||||
// Reload the extension settings
|
// Reload the extension settings
|
||||||
PreferenceSubscreenExtensionsLoad();
|
PreferenceSubscreenExtensionsLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unloads the preference subscreen for extensions
|
||||||
|
* Cleans up the current extension, and reset the current extension to null
|
||||||
|
*/
|
||||||
|
function PreferenceSubscreenExtensionsUnload() {
|
||||||
|
PreferenceExtensionsCurrent?.unload?.();
|
||||||
|
PreferenceExtensionsCurrent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,8 +103,8 @@ function PreferenceSubscreenGeneralClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exits the preference screen. Cleans up elements that are not needed anymore
|
* Exits the preference screen. Block exit when the color picker is active.
|
||||||
* If the selected color is invalid, the player cannot leave the screen.
|
* @returns {boolean} - Returns false if the color picker is active and input is not valid
|
||||||
*/
|
*/
|
||||||
function PreferenceSubscreenGeneralExit() {
|
function PreferenceSubscreenGeneralExit() {
|
||||||
if (PreferenceSubscreenGeneralColorPicker) return false;
|
if (PreferenceSubscreenGeneralColorPicker) return false;
|
||||||
|
@ -115,13 +115,23 @@ function PreferenceSubscreenGeneralExit() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (color !== Player.LabelColor) {
|
|
||||||
Player.LabelColor = color;
|
|
||||||
const elems = /** @type {HTMLElement[]} */(Array.from(document.querySelectorAll(`[style*="--label-color"][data-sender="${Player.MemberNumber}"]`)));
|
|
||||||
elems.forEach(e => e.style.setProperty("--label-color", color));
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferenceMessage = "";
|
|
||||||
ElementRemove("InputCharacterLabelColor");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up elements that are not needed anymore
|
||||||
|
* If the selected color is invalid, the player cannot leave the screen.
|
||||||
|
*/
|
||||||
|
function PreferenceSubscreenGeneralUnload() {
|
||||||
|
const color = ElementValue("InputCharacterLabelColor");
|
||||||
|
if (CommonIsColor(color)) {
|
||||||
|
if (color !== Player.LabelColor) {
|
||||||
|
Player.LabelColor = color;
|
||||||
|
const elems = /** @type {HTMLElement[]} */(Array.from(document.querySelectorAll(`[style*="--label-color"][data-sender="${Player.MemberNumber}"]`)));
|
||||||
|
elems.forEach(e => e.style.setProperty("--label-color", color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PreferenceMessage = "";
|
||||||
|
ElementRemove("InputCharacterLabelColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -211,6 +211,13 @@ function PreferenceSubscreenGraphicsClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PreferenceSubscreenGraphicsExit() {
|
function PreferenceSubscreenGraphicsExit() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalize graphics setting when the screen is unloaded
|
||||||
|
*/
|
||||||
|
function PreferenceSubscreenGraphicsUnload() {
|
||||||
// Reload WebGL if graphic settings have changed.
|
// Reload WebGL if graphic settings have changed.
|
||||||
const currentOptions = GLDrawGetOptions();
|
const currentOptions = GLDrawGetOptions();
|
||||||
if (
|
if (
|
||||||
|
@ -223,5 +230,4 @@ function PreferenceSubscreenGraphicsExit() {
|
||||||
GLDrawSetOptions(PreferenceGraphicsWebGLOptions);
|
GLDrawSetOptions(PreferenceGraphicsWebGLOptions);
|
||||||
GLDrawResetCanvas();
|
GLDrawResetCanvas();
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,7 +185,13 @@ function PreferenceNotificationsClickSetting(Left, Top, Setting, EventType) {
|
||||||
* Exits the preference screen. Resets the test notifications.
|
* Exits the preference screen. Resets the test notifications.
|
||||||
*/
|
*/
|
||||||
function PreferenceSubscreenNotificationsExit() {
|
function PreferenceSubscreenNotificationsExit() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalize notification setting when the screen is unloaded
|
||||||
|
*/
|
||||||
|
function PreferenceSubscreenNotificationsUnload() {
|
||||||
// If any of the settings now have audio enabled, enable the AudioSettings setting as well
|
// If any of the settings now have audio enabled, enable the AudioSettings setting as well
|
||||||
let enableAudio = false;
|
let enableAudio = false;
|
||||||
for (const setting in Player.NotificationSettings) {
|
for (const setting in Player.NotificationSettings) {
|
||||||
|
@ -195,5 +201,4 @@ function PreferenceSubscreenNotificationsExit() {
|
||||||
if (enableAudio) Player.AudioSettings.Notifications = true;
|
if (enableAudio) Player.AudioSettings.Notifications = true;
|
||||||
|
|
||||||
NotificationReset(NotificationEventType.TEST);
|
NotificationReset(NotificationEventType.TEST);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenGeneralRun(),
|
run: () => PreferenceSubscreenGeneralRun(),
|
||||||
click: () => PreferenceSubscreenGeneralClick(),
|
click: () => PreferenceSubscreenGeneralClick(),
|
||||||
exit: () => PreferenceSubscreenGeneralExit(),
|
exit: () => PreferenceSubscreenGeneralExit(),
|
||||||
|
unload: () => PreferenceSubscreenGeneralUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Difficulty",
|
name: "Difficulty",
|
||||||
|
@ -64,6 +65,7 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenCensoredWordsRun(),
|
run: () => PreferenceSubscreenCensoredWordsRun(),
|
||||||
click: () => PreferenceSubscreenCensoredWordsClick(),
|
click: () => PreferenceSubscreenCensoredWordsClick(),
|
||||||
exit: () => PreferenceSubscreenCensoredWordsExit(),
|
exit: () => PreferenceSubscreenCensoredWordsExit(),
|
||||||
|
unload: () => PreferenceSubscreenCensoredWordsUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Audio",
|
name: "Audio",
|
||||||
|
@ -71,6 +73,7 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenAudioRun(),
|
run: () => PreferenceSubscreenAudioRun(),
|
||||||
click: () => PreferenceSubscreenAudioClick(),
|
click: () => PreferenceSubscreenAudioClick(),
|
||||||
exit: () => PreferenceSubscreenAudioExit(),
|
exit: () => PreferenceSubscreenAudioExit(),
|
||||||
|
unload: () => PreferenceSubscreenAudioUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Arousal",
|
name: "Arousal",
|
||||||
|
@ -78,6 +81,7 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenArousalRun(),
|
run: () => PreferenceSubscreenArousalRun(),
|
||||||
click: () => PreferenceSubscreenArousalClick(),
|
click: () => PreferenceSubscreenArousalClick(),
|
||||||
exit: () => PreferenceSubscreenArousalExit(),
|
exit: () => PreferenceSubscreenArousalExit(),
|
||||||
|
unload: () => PreferenceSubscreenArousalUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Security",
|
name: "Security",
|
||||||
|
@ -85,6 +89,7 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenSecurityRun(),
|
run: () => PreferenceSubscreenSecurityRun(),
|
||||||
click: () => PreferenceSubscreenSecurityClick(),
|
click: () => PreferenceSubscreenSecurityClick(),
|
||||||
exit: () => PreferenceSubscreenSecurityExit(),
|
exit: () => PreferenceSubscreenSecurityExit(),
|
||||||
|
unload: () => PreferenceSubscreenSecurityUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Online",
|
name: "Online",
|
||||||
|
@ -97,6 +102,7 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenVisibilityRun(),
|
run: () => PreferenceSubscreenVisibilityRun(),
|
||||||
click: () => PreferenceSubscreenVisibilityClick(),
|
click: () => PreferenceSubscreenVisibilityClick(),
|
||||||
exit: () => PreferenceSubscreenVisibilityExit(),
|
exit: () => PreferenceSubscreenVisibilityExit(),
|
||||||
|
unload: () => PreferenceSubscreenVisibilityUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Immersion",
|
name: "Immersion",
|
||||||
|
@ -110,12 +116,14 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenGraphicsRun(),
|
run: () => PreferenceSubscreenGraphicsRun(),
|
||||||
click: () => PreferenceSubscreenGraphicsClick(),
|
click: () => PreferenceSubscreenGraphicsClick(),
|
||||||
exit: () => PreferenceSubscreenGraphicsExit(),
|
exit: () => PreferenceSubscreenGraphicsExit(),
|
||||||
|
unload: () => PreferenceSubscreenGraphicsUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Controller",
|
name: "Controller",
|
||||||
run: () => PreferenceSubscreenControllerRun(),
|
run: () => PreferenceSubscreenControllerRun(),
|
||||||
click: () => PreferenceSubscreenControllerClick(),
|
click: () => PreferenceSubscreenControllerClick(),
|
||||||
exit: () => PreferenceSubscreenControllerExit(),
|
exit: () => PreferenceSubscreenControllerExit(),
|
||||||
|
unload: () => PreferenceSubscreenControllerUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Notifications",
|
name: "Notifications",
|
||||||
|
@ -123,6 +131,7 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenNotificationsRun(),
|
run: () => PreferenceSubscreenNotificationsRun(),
|
||||||
click: () => PreferenceSubscreenNotificationsClick(),
|
click: () => PreferenceSubscreenNotificationsClick(),
|
||||||
exit: () => PreferenceSubscreenNotificationsExit(),
|
exit: () => PreferenceSubscreenNotificationsExit(),
|
||||||
|
unload: () => PreferenceSubscreenNotificationsUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Gender",
|
name: "Gender",
|
||||||
|
@ -135,6 +144,7 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenScriptsRun(),
|
run: () => PreferenceSubscreenScriptsRun(),
|
||||||
click: () => PreferenceSubscreenScriptsClick(),
|
click: () => PreferenceSubscreenScriptsClick(),
|
||||||
exit: () => PreferenceSubscreenScriptsExit(),
|
exit: () => PreferenceSubscreenScriptsExit(),
|
||||||
|
unload: () => PreferenceSubscreenScriptsUnload(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Extensions",
|
name: "Extensions",
|
||||||
|
@ -142,6 +152,7 @@ const PreferenceSubscreens = [
|
||||||
run: () => PreferenceSubscreenExtensionsRun(),
|
run: () => PreferenceSubscreenExtensionsRun(),
|
||||||
click: () => PreferenceSubscreenExtensionsClick(),
|
click: () => PreferenceSubscreenExtensionsClick(),
|
||||||
exit: () => PreferenceSubscreenExtensionsExit(),
|
exit: () => PreferenceSubscreenExtensionsExit(),
|
||||||
|
unload: () => PreferenceSubscreenExtensionsUnload(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -217,11 +228,14 @@ function PreferenceClick() {
|
||||||
*/
|
*/
|
||||||
function PreferenceExit() {
|
function PreferenceExit() {
|
||||||
if (PreferenceSubscreen.name !== "Main") {
|
if (PreferenceSubscreen.name !== "Main") {
|
||||||
if (PreferenceSubscreenExit())
|
// If we are in a subscreen, the only exit is to the main preference screen
|
||||||
return;
|
PreferenceSubscreenExit();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit the preference menus
|
// Exit the preference menus
|
||||||
|
// Only a normal exit triggers an update to server. so we don't send data in unload function,
|
||||||
|
// which could be called from disconnects
|
||||||
const P = {
|
const P = {
|
||||||
ArousalSettings: Player.ArousalSettings,
|
ArousalSettings: Player.ArousalSettings,
|
||||||
ChatSettings: Player.ChatSettings,
|
ChatSettings: Player.ChatSettings,
|
||||||
|
@ -245,19 +259,31 @@ function PreferenceExit() {
|
||||||
CommonSetScreen("Character", "InformationSheet");
|
CommonSetScreen("Character", "InformationSheet");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all GUI data and DOM elements creates by the preference screen load function
|
||||||
|
* We don't do this in exit function for disconnects do not trigger the exit function
|
||||||
|
*/
|
||||||
|
function PreferenceUnload() {
|
||||||
|
if (PreferenceSubscreen.name !== "Main") {
|
||||||
|
PreferenceSubscreen.unload?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exit from a specific subscreen by running its handler and checking its validity
|
* Exit from a specific subscreen by running its handler and checking its validity
|
||||||
*/
|
*/
|
||||||
function PreferenceSubscreenExit() {
|
function PreferenceSubscreenExit() {
|
||||||
let valid = true;
|
const validExit = PreferenceSubscreen.exit?.();
|
||||||
if (PreferenceSubscreen.exit)
|
|
||||||
valid = PreferenceSubscreen.exit();
|
|
||||||
|
|
||||||
if (!valid) return valid;
|
// Only when the results is false (not undefined)
|
||||||
|
// The exit is just a exit of the subscreen's substate, return to block more exit.
|
||||||
|
if(validExit === false) return;
|
||||||
|
|
||||||
|
// The exit is a full exit of the subscreen, unload resources
|
||||||
|
PreferenceSubscreen.unload?.();
|
||||||
|
|
||||||
PreferenceMessage = "";
|
PreferenceMessage = "";
|
||||||
PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === "Main");
|
PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === "Main");
|
||||||
return valid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -190,6 +190,10 @@ function PreferenceSubscreenScriptsClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PreferenceSubscreenScriptsExit() {
|
function PreferenceSubscreenScriptsExit() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PreferenceSubscreenScriptsUnload() {
|
||||||
if (PreferenceScriptTimeoutHandle != null) {
|
if (PreferenceScriptTimeoutHandle != null) {
|
||||||
clearTimeout(PreferenceScriptTimeoutHandle);
|
clearTimeout(PreferenceScriptTimeoutHandle);
|
||||||
PreferenceScriptTimeoutHandle = null;
|
PreferenceScriptTimeoutHandle = null;
|
||||||
|
@ -215,7 +219,6 @@ function PreferenceSubscreenScriptsExit() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function PreferenceSubscreenScriptsWarningClick() {
|
function PreferenceSubscreenScriptsWarningClick() {
|
||||||
|
|
|
@ -54,7 +54,10 @@ function PreferenceSubscreenSecurityClick() {
|
||||||
* Exits the preference screen
|
* Exits the preference screen
|
||||||
*/
|
*/
|
||||||
function PreferenceSubscreenSecurityExit() {
|
function PreferenceSubscreenSecurityExit() {
|
||||||
ElementRemove("InputEmailOld");
|
|
||||||
ElementRemove("InputEmailNew");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PreferenceSubscreenSecurityUnload() {
|
||||||
|
ElementRemove("InputEmailOld");
|
||||||
|
ElementRemove("InputEmailNew");
|
||||||
|
}
|
||||||
|
|
|
@ -233,9 +233,13 @@ function PreferenceVisibilityCheckboxChanged(permissionRecord, CheckSetting, Typ
|
||||||
* Exits the preference screen
|
* Exits the preference screen
|
||||||
*/
|
*/
|
||||||
function PreferenceSubscreenVisibilityExit() {
|
function PreferenceSubscreenVisibilityExit() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function PreferenceSubscreenVisibilityUnload() {
|
||||||
PreferenceVisibilityGroupList = [];
|
PreferenceVisibilityGroupList = [];
|
||||||
PreferenceVisibilityRecord = {};
|
PreferenceVisibilityRecord = {};
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Binary file not shown.
Before ![]() (image error) Size: 53 KiB |
Binary file not shown.
Before ![]() (image error) Size: 91 KiB |
|
@ -1159,11 +1159,7 @@ function CraftingLoad() {
|
||||||
attributes: { id: CraftingID.privateLabel },
|
attributes: { id: CraftingID.privateLabel },
|
||||||
classList: ["crafting-label"],
|
classList: ["crafting-label"],
|
||||||
children: [
|
children: [
|
||||||
{
|
ElementCheckbox.Create(CraftingID.privateCheckbox, CraftingEventListeners._ClickPrivate),
|
||||||
tag: "input",
|
|
||||||
attributes: { id: CraftingID.privateCheckbox, type: "checkbox" },
|
|
||||||
eventListeners: { click: CraftingEventListeners._ClickPrivate },
|
|
||||||
},
|
|
||||||
{ tag: "span", children: [TextGet("EnterPrivate")] },
|
{ tag: "span", children: [TextGet("EnterPrivate")] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -1190,11 +1186,7 @@ function CraftingLoad() {
|
||||||
attributes: { id: CraftingID.asciidescriptionLabel },
|
attributes: { id: CraftingID.asciidescriptionLabel },
|
||||||
classList: ["crafting-label"],
|
classList: ["crafting-label"],
|
||||||
children: [
|
children: [
|
||||||
{
|
ElementCheckbox.Create(CraftingID.asciiDescriptionCheckbox, CraftingEventListeners._ClickAsciiDescription),
|
||||||
tag: "input",
|
|
||||||
attributes: { id: CraftingID.asciiDescriptionCheckbox, type: "checkbox" },
|
|
||||||
eventListeners: { click: CraftingEventListeners._ClickAsciiDescription },
|
|
||||||
},
|
|
||||||
{ tag: "span", children: [TextGet("EnterExtendedDescription")] },
|
{ tag: "span", children: [TextGet("EnterExtendedDescription")] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -258,7 +258,7 @@ function ActivityCheckPrerequisites(activity, acting, acted, group) {
|
||||||
* @param {ItemActivity[]} allowed
|
* @param {ItemActivity[]} allowed
|
||||||
* @param {Character} acting
|
* @param {Character} acting
|
||||||
* @param {Character} acted
|
* @param {Character} acted
|
||||||
* @param {string} needsItem
|
* @param {ActivityNameItem} needsItem
|
||||||
* @param {Activity} activity
|
* @param {Activity} activity
|
||||||
* @param {AssetGroup} targetGroup
|
* @param {AssetGroup} targetGroup
|
||||||
*/
|
*/
|
||||||
|
@ -342,7 +342,8 @@ function ActivityAllowedForGroup(character, groupname) {
|
||||||
let needsItem = activity.Prerequisite.find(p => p.startsWith("Needs-"));
|
let needsItem = activity.Prerequisite.find(p => p.startsWith("Needs-"));
|
||||||
|
|
||||||
if (needsItem) {
|
if (needsItem) {
|
||||||
handled = ActivityGenerateItemActivitiesFromNeed(allowed, Player, character, needsItem.substring(6), activity, group);
|
const needsItemActivity = /** @type {ActivityNameItem} */(needsItem.substring(6));
|
||||||
|
handled = ActivityGenerateItemActivitiesFromNeed(allowed, Player, character, needsItemActivity, activity, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activity.Name === "ShockItem" && InventoryItemHasEffect(targetedItem, "ReceiveShock")) {
|
if (activity.Name === "ShockItem" && InventoryItemHasEffect(targetedItem, "ReceiveShock")) {
|
||||||
|
|
|
@ -1930,7 +1930,7 @@ function CharacterHasItemWithAttribute(C, Attribute) {
|
||||||
/**
|
/**
|
||||||
* Checks if the character is wearing an item that allows for a specific activity
|
* Checks if the character is wearing an item that allows for a specific activity
|
||||||
* @param {Character} C - The character to test for
|
* @param {Character} C - The character to test for
|
||||||
* @param {string} Activity - The name of the activity that must be allowed
|
* @param {ActivityName} Activity - The name of the activity that must be allowed
|
||||||
* @returns {Item[]} - A list of items allowing that activity
|
* @returns {Item[]} - A list of items allowing that activity
|
||||||
*/
|
*/
|
||||||
function CharacterItemsForActivity(C, Activity) {
|
function CharacterItemsForActivity(C, Activity) {
|
||||||
|
@ -1942,7 +1942,7 @@ function CharacterItemsForActivity(C, Activity) {
|
||||||
/**
|
/**
|
||||||
* Checks if the character is wearing an item that allows for a specific activity
|
* Checks if the character is wearing an item that allows for a specific activity
|
||||||
* @param {Character} C - The character to test for
|
* @param {Character} C - The character to test for
|
||||||
* @param {String} Activity - The name of the activity that must be allowed
|
* @param {ActivityName} Activity - The name of the activity that must be allowed
|
||||||
* @returns {boolean} - TRUE if at least one item allows that activity
|
* @returns {boolean} - TRUE if at least one item allows that activity
|
||||||
*/
|
*/
|
||||||
function CharacterHasItemForActivity(C, Activity) {
|
function CharacterHasItemForActivity(C, Activity) {
|
||||||
|
|
|
@ -2731,6 +2731,23 @@ function DialogDrawItemMenu(C) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Abstract base class for a simplistic DOM subscreen with three-ish components:
|
||||||
|
* - A menubar with a set of buttons which are generally heterogeneous in function (_e.g._ perform arbitrary, unrelated task #1, #2 or #3)
|
||||||
|
* - A status message of some sort
|
||||||
|
* - A grid with some type of misc element, generally a set of buttons homogeneous in function (e.g. equip item #1, #2 or #3). See below for more details.
|
||||||
|
*
|
||||||
|
* Grid button clicks
|
||||||
|
* ------------------
|
||||||
|
* Grid button clicks in the {@link ids|ids.grid}-referenced element generally involve the following four steps:
|
||||||
|
* 1) The click listener (see {@link eventListeners|eventListeners._ClickButton}) performs some basic generic validation, like checking whether the character has been initialized.
|
||||||
|
* A validation failure is considered an internal error, and will lead to a premature termination of the click event.
|
||||||
|
* 2) The click listener retrieves some type of underlying object associated with the grid button, like an item or activity (see {@link _GetClickedObject}).
|
||||||
|
* 3) The click listener performs more extensive, subscreen-/class-specific validation (see {@link GetClickStatus}), like checking whether an item has not been blacklisted.
|
||||||
|
* A validation failure here will trigger a soft reload, updating the status message and re-evaluating the enabled/disabled state of _all_ pre-existing grid buttons.
|
||||||
|
* 4) The click listener finally performs a subscreen-/class-specific action based on the grid button click, like equipping an item (see {@link _ClickButton}).
|
||||||
|
*
|
||||||
|
* Parameters
|
||||||
|
* ----------
|
||||||
* @abstract
|
* @abstract
|
||||||
* @template {string} [ModeType=string] - The name of the mode associated with this instance (_e.g._ {@link DialogMenuMode})
|
* @template {string} [ModeType=string] - The name of the mode associated with this instance (_e.g._ {@link DialogMenuMode})
|
||||||
* @template [ClickedObj=any] - The underlying item or activity object of the clicked grid buttons (if applicable)
|
* @template [ClickedObj=any] - The underlying item or activity object of the clicked grid buttons (if applicable)
|
||||||
|
|
|
@ -338,7 +338,7 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
|
||||||
// If we must dark the Canvas characters
|
// If we must dark the Canvas characters
|
||||||
if (!C.IsPlayer() && !OverrideDark && (Player.IsBlind() || Player.HasTints())) {
|
if (!C.IsPlayer() && !OverrideDark && (Player.IsBlind() || Player.HasTints())) {
|
||||||
TempCanvas.canvas.width = CanvasDrawWidth;
|
TempCanvas.canvas.width = CanvasDrawWidth;
|
||||||
TempCanvas.canvas.height = CanvasDrawHeight;
|
TempCanvas.canvas.height = CanvasDrawHeight;
|
||||||
|
|
||||||
TempCanvas.globalCompositeOperation = "copy";
|
TempCanvas.globalCompositeOperation = "copy";
|
||||||
TempCanvas.drawImage(Canvas, 0, 0);
|
TempCanvas.drawImage(Canvas, 0, 0);
|
||||||
|
@ -352,7 +352,7 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tints = Player.GetTints();
|
const Tints = Player.GetTints();
|
||||||
for (const {r, g, b, a} of Tints) {
|
for (const { r, g, b, a } of Tints) {
|
||||||
TempCanvas.fillStyle = `rgba(${r},${g},${b},${a})`;
|
TempCanvas.fillStyle = `rgba(${r},${g},${b},${a})`;
|
||||||
TempCanvas.fillRect(0, 0, Canvas.width, Canvas.height);
|
TempCanvas.fillRect(0, 0, Canvas.width, Canvas.height);
|
||||||
}
|
}
|
||||||
|
@ -376,7 +376,8 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
|
||||||
|
|
||||||
// Apply blur filter if needed
|
// Apply blur filter if needed
|
||||||
const BlurLevel = Player.GetBlurLevel();
|
const BlurLevel = Player.GetBlurLevel();
|
||||||
if (!C.IsPlayer() && !OverrideDark && BlurLevel > 0) {
|
const needsBlur = !C.IsPlayer() && !OverrideDark && BlurLevel > 0;
|
||||||
|
if (needsBlur) {
|
||||||
MainCanvas.filter = `blur(${BlurLevel}px)`;
|
MainCanvas.filter = `blur(${BlurLevel}px)`;
|
||||||
}
|
}
|
||||||
// Draw the character
|
// Draw the character
|
||||||
|
@ -387,7 +388,10 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
|
||||||
Invert: IsInverted,
|
Invert: IsInverted,
|
||||||
Mirror: IsInverted
|
Mirror: IsInverted
|
||||||
});
|
});
|
||||||
MainCanvas.filter = 'none';
|
// Resetting the filter is expensive, even if there's no change. Only change the filter if we need to.
|
||||||
|
if (needsBlur) {
|
||||||
|
MainCanvas.filter = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Draw the arousal meter & game images on certain conditions
|
// Draw the arousal meter & game images on certain conditions
|
||||||
if (CurrentScreen != "ChatRoom" || ChatRoomHideIconState <= 1) {
|
if (CurrentScreen != "ChatRoom" || ChatRoomHideIconState <= 1) {
|
||||||
|
@ -683,24 +687,21 @@ function DrawImageEx(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blit the transformed image to the main canvas, applying opacity and zoom
|
// Blit the transformed image to the main canvas, applying opacity and zoom
|
||||||
Canvas.save();
|
|
||||||
|
// Instead of expensive save/restore, we store the relevant state info and restore it manually
|
||||||
|
let savedCompositeOperation = Canvas.globalCompositeOperation;
|
||||||
|
let savedAlpha = Canvas.globalAlpha;
|
||||||
|
|
||||||
Canvas.globalCompositeOperation = BlendingMode;
|
Canvas.globalCompositeOperation = BlendingMode;
|
||||||
Canvas.globalAlpha = Alpha;
|
Canvas.globalAlpha = Alpha;
|
||||||
|
|
||||||
Canvas.translate(X, Y);
|
// Performance benefits from combining transforms is usually minimal to none but in cases with multiple transforms it adds up
|
||||||
|
const scaleHoriz = Zoom * (Mirror ? -1 : 1); // Scaling and horizontal mirroring
|
||||||
|
const scaleVert = Zoom * (Invert ? -1 : 1); // Scaling and vertical inversion
|
||||||
|
const translateX = X + (Mirror ? Width : 0); // Translation in x
|
||||||
|
const translateY = Y + (Invert ? Height : 0); // Translation in y
|
||||||
|
|
||||||
if (Zoom != 1) {
|
Canvas.transform(scaleHoriz, 0, 0, scaleVert, translateX, translateY);
|
||||||
Canvas.scale(Zoom, Zoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Invert) {
|
|
||||||
Canvas.transform(1, 0, 0, -1, 0, Height);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Mirror) {
|
|
||||||
Canvas.transform(-1, 0, 0, 1, Width, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SourcePos) {
|
if (SourcePos) {
|
||||||
Canvas.drawImage(destCanvas, SourcePos[0], SourcePos[1], SourcePos[2], SourcePos[3], 0, 0, Width, Height);
|
Canvas.drawImage(destCanvas, SourcePos[0], SourcePos[1], SourcePos[2], SourcePos[3], 0, 0, Width, Height);
|
||||||
|
@ -710,7 +711,9 @@ function DrawImageEx(
|
||||||
Canvas.drawImage(destCanvas, 0, 0);
|
Canvas.drawImage(destCanvas, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Canvas.restore();
|
Canvas.globalCompositeOperation = savedCompositeOperation;
|
||||||
|
Canvas.globalAlpha = savedAlpha;
|
||||||
|
Canvas.resetTransform();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -799,7 +802,7 @@ function GetWrapTextSize(Text, Width, MaxLine) {
|
||||||
* @param {"Center" | "Top"} Alignment - How the text should be alligned w.r.t. the Y position when wrapped over multiple lines
|
* @param {"Center" | "Top"} Alignment - How the text should be alligned w.r.t. the Y position when wrapped over multiple lines
|
||||||
* @returns {void} - Nothing
|
* @returns {void} - Nothing
|
||||||
*/
|
*/
|
||||||
function DrawTextWrap(Text, X, Y, Width, Height, ForeColor, BackColor, MaxLine, LineSpacing=23, Alignment="Center") {
|
function DrawTextWrap(Text, X, Y, Width, Height, ForeColor, BackColor, MaxLine, LineSpacing = 23, Alignment = "Center") {
|
||||||
ControllerAddActiveArea(X, Y);
|
ControllerAddActiveArea(X, Y);
|
||||||
|
|
||||||
// Draw the rectangle if we need too
|
// Draw the rectangle if we need too
|
||||||
|
@ -912,7 +915,7 @@ function DrawTextFit(Text, X, Y, Width, Color, BackColor) {
|
||||||
const DrawingGetTextSize = CommonMemoize(
|
const DrawingGetTextSize = CommonMemoize(
|
||||||
/** @type {(Text: string, width: number) => [text: string, size: number]} */
|
/** @type {(Text: string, width: number) => [text: string, size: number]} */
|
||||||
(Text, Width) => {
|
(Text, Width) => {
|
||||||
// If it doesn't fit, test with smaller and smaller fonts until it fits
|
// If it doesn't fit, test with smaller and smaller fonts until it fits
|
||||||
let S;
|
let S;
|
||||||
for (S = 36; S >= 10; S = S - 2) {
|
for (S = 36; S >= 10; S = S - 2) {
|
||||||
MainCanvas.font = CommonGetFont(S.toString());
|
MainCanvas.font = CommonGetFont(S.toString());
|
||||||
|
@ -1277,7 +1280,10 @@ function DrawRoomBackground(URL, bounds, opts) {
|
||||||
DrawRect(...destRect, ChatRoomCustomFilter);
|
DrawRect(...destRect, ChatRoomCustomFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
MainCanvas.filter = 'none';
|
// Resetting the filter is expensive, even if there's no change. Only change the filter if we need to.
|
||||||
|
if (blur > 0) {
|
||||||
|
MainCanvas.filter = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Draw an overlay if the character is partially blinded
|
// Draw an overlay if the character is partially blinded
|
||||||
if (darken < 1) {
|
if (darken < 1) {
|
||||||
|
@ -1288,7 +1294,7 @@ function DrawRoomBackground(URL, bounds, opts) {
|
||||||
DrawRect(...RectGetFrame(bounds), "#000");
|
DrawRect(...RectGetFrame(bounds), "#000");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const {r, g, b, a} of tints) {
|
for (const { r, g, b, a } of tints) {
|
||||||
DrawRect(bounds.x, bounds.y, bounds.w, bounds.h, `rgba(${r},${g},${b},${a})`);
|
DrawRect(bounds.x, bounds.y, bounds.w, bounds.h, `rgba(${r},${g},${b},${a})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1325,7 +1331,7 @@ function DrawFlashScreen(Color, Duration, Intensity) {
|
||||||
* @returns {string} - alpha of screen flash
|
* @returns {string} - alpha of screen flash
|
||||||
*/
|
*/
|
||||||
function DrawGetScreenFlashAlpha(FlashTime) {
|
function DrawGetScreenFlashAlpha(FlashTime) {
|
||||||
let alpha = Math.max(0, Math.min(255, Math.floor(DrawScreenFlashStrength * (1 - Math.exp(-FlashTime/2500))))).toString(16);
|
let alpha = Math.max(0, Math.min(255, Math.floor(DrawScreenFlashStrength * (1 - Math.exp(-FlashTime / 2500))))).toString(16);
|
||||||
if (alpha.length < 2) alpha = "0" + alpha;
|
if (alpha.length < 2) alpha = "0" + alpha;
|
||||||
return alpha;
|
return alpha;
|
||||||
}
|
}
|
||||||
|
@ -1457,7 +1463,7 @@ function DrawProcessScreenFlash() {
|
||||||
if (BlindFlash == true && CurrentTime < DrawingBlindFlashTimer) {
|
if (BlindFlash == true && CurrentTime < DrawingBlindFlashTimer) {
|
||||||
if (Player.GetBlindLevel() == 0) {
|
if (Player.GetBlindLevel() == 0) {
|
||||||
let FlashTime = DrawingBlindFlashTimer - CurrentTime;
|
let FlashTime = DrawingBlindFlashTimer - CurrentTime;
|
||||||
DrawRect(0, 0, 2000, 1000, "#ffffff" + DrawGetScreenFlashAlpha(FlashTime/Math.max(1, 4 - DrawLastDarkFactor)));
|
DrawRect(0, 0, 2000, 1000, "#ffffff" + DrawGetScreenFlashAlpha(FlashTime / Math.max(1, 4 - DrawLastDarkFactor)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1527,7 +1533,7 @@ function DrawAssetPreview(X, Y, A, Options) {
|
||||||
|
|
||||||
if (Description == null) Description = C ? A.DynamicDescription(C) : A.Description;
|
if (Description == null) Description = C ? A.DynamicDescription(C) : A.Description;
|
||||||
|
|
||||||
DrawPreviewBox(X, Y, Path, Description, { Background, Foreground, Vibrating, Border, Hover, HoverBackground, Disabled, Icons, Width, Height});
|
DrawPreviewBox(X, Y, Path, Description, { Background, Foreground, Vibrating, Border, Hover, HoverBackground, Disabled, Icons, Width, Height });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The default width of item previews */
|
/** The default width of item previews */
|
||||||
|
@ -1679,7 +1685,7 @@ function DrawCharacterSegment(C, Left, Top, Width, Height) {
|
||||||
* @param {number} [y] - The y-position on the target canvas that the final image should be drawn at
|
* @param {number} [y] - The y-position on the target canvas that the final image should be drawn at
|
||||||
*/
|
*/
|
||||||
function DrawImageTrapezify(image, targetCanvas, topToBottomRatio, x = 0, y = 0) {
|
function DrawImageTrapezify(image, targetCanvas, topToBottomRatio, x = 0, y = 0) {
|
||||||
const {width, height} = image;
|
const { width, height } = image;
|
||||||
let xStartTop = 0;
|
let xStartTop = 0;
|
||||||
let xStartBottom = 0;
|
let xStartBottom = 0;
|
||||||
|
|
||||||
|
|
|
@ -686,12 +686,7 @@ var ElementCheckboxDropdown = {
|
||||||
classList: ["dropdown-checkbox-grid"],
|
classList: ["dropdown-checkbox-grid"],
|
||||||
attributes: { id: `${idPrefix}-pair-${idSuffix}` },
|
attributes: { id: `${idPrefix}-pair-${idSuffix}` },
|
||||||
children: [
|
children: [
|
||||||
{
|
ElementCheckbox.Create(`${idPrefix}-checkbox-${idSuffix}`, listener, { checked }),
|
||||||
tag: "input",
|
|
||||||
classList: ["dropdown-checkbox"],
|
|
||||||
attributes: { id: `${idPrefix}-checkbox-${idSuffix}`, type: "checkbox", checked },
|
|
||||||
eventListeners: { click: listener },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
tag: "span",
|
tag: "span",
|
||||||
classList: ["dropdown-checkbox-label"],
|
classList: ["dropdown-checkbox-label"],
|
||||||
|
@ -807,6 +802,12 @@ function ElementCreateSearchInput(id, dataCallback, options=null) {
|
||||||
* @namespace
|
* @namespace
|
||||||
*/
|
*/
|
||||||
var ElementButton = {
|
var ElementButton = {
|
||||||
|
/**
|
||||||
|
* A unique element ID-suffix to-be assigned to buttons without an explicit ID.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_idCounter: 0,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @readonly
|
* @readonly
|
||||||
|
@ -1217,6 +1218,7 @@ var ElementButton = {
|
||||||
* @returns {HTMLButtonElement} - The created button
|
* @returns {HTMLButtonElement} - The created button
|
||||||
*/
|
*/
|
||||||
Create: function Create(id, onClick, options=null, htmlOptions=null) {
|
Create: function Create(id, onClick, options=null, htmlOptions=null) {
|
||||||
|
id ??= `button-${ElementButton._idCounter++}`;
|
||||||
let elem = /** @type {HTMLButtonElement | null} */(document.getElementById(id));
|
let elem = /** @type {HTMLButtonElement | null} */(document.getElementById(id));
|
||||||
if (elem) {
|
if (elem) {
|
||||||
console.error(`Element "${id}" already exists`);
|
console.error(`Element "${id}" already exists`);
|
||||||
|
@ -1227,7 +1229,6 @@ var ElementButton = {
|
||||||
const buttonOptions = htmlOptions.button ?? {};
|
const buttonOptions = htmlOptions.button ?? {};
|
||||||
const tooltipOptions = htmlOptions.tooltip ?? {};
|
const tooltipOptions = htmlOptions.tooltip ?? {};
|
||||||
|
|
||||||
id ??= `button-${Date.now()}`;
|
|
||||||
options ??= {};
|
options ??= {};
|
||||||
const image = this._ParseImage(id, options.image, htmlOptions.img);
|
const image = this._ParseImage(id, options.image, htmlOptions.img);
|
||||||
const label = this._ParseLabel(id, options.label, options.labelPosition, htmlOptions.label);
|
const label = this._ParseLabel(id, options.label, options.labelPosition, htmlOptions.label);
|
||||||
|
@ -1798,6 +1799,53 @@ var ElementMenu = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace for creating DOM checkboxes.
|
||||||
|
*/
|
||||||
|
var ElementCheckbox = {
|
||||||
|
/**
|
||||||
|
* A unique element ID-suffix to-be assigned to checkboxes without an explicit ID.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_idCounter: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an return a DOM checkbox element (`<input type="checkbox">`)
|
||||||
|
* @param {null | string} id - The ID of the element, or `null` if one must be assigned automatically
|
||||||
|
* @param {(this: HTMLInputElement, ev: Event) => any} onChange - The change event listener to-be fired upon checkbox clicks
|
||||||
|
* @param {null | ElementCheckbox.Options} options - High level options for the to-be created checkbox
|
||||||
|
* @param {null | Partial<Record<"checkbox", Omit<HTMLOptions<any>, "tag">>>} htmlOptions - Additional {@link ElementCreate} options to-be applied to the respective (child) element
|
||||||
|
*/
|
||||||
|
Create: function Create(id, onChange, options=null, htmlOptions=null) {
|
||||||
|
id ??= `checkbox-${ElementCheckbox._idCounter++}`;
|
||||||
|
const checkbox = document.getElementById(id);
|
||||||
|
if (checkbox) {
|
||||||
|
console.error(`Element "${id}" already exists`);
|
||||||
|
return checkbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
options ??= {};
|
||||||
|
const checkboxOptions = htmlOptions?.checkbox ?? {};
|
||||||
|
return ElementCreate({
|
||||||
|
...checkboxOptions,
|
||||||
|
tag: "input",
|
||||||
|
attributes: {
|
||||||
|
id,
|
||||||
|
type: "checkbox",
|
||||||
|
disabled: options.disabled,
|
||||||
|
checked: options.checked,
|
||||||
|
value: options.value,
|
||||||
|
...(checkboxOptions.attributes ?? {}),
|
||||||
|
},
|
||||||
|
classList: ["checkbox", ...(checkboxOptions.classList ?? [])],
|
||||||
|
eventListeners: {
|
||||||
|
change: onChange,
|
||||||
|
...(checkboxOptions.eventListeners ?? {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return whether an element is visible or not.
|
* Return whether an element is visible or not.
|
||||||
*
|
*
|
||||||
|
|
38
BondageClub/Scripts/Typedef.d.ts
vendored
38
BondageClub/Scripts/Typedef.d.ts
vendored
|
@ -179,6 +179,23 @@ declare namespace ElementButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare namespace ElementCheckbox {
|
||||||
|
|
||||||
|
/** Various options that can be passed along to {@link ElementCheckbox.Create} */
|
||||||
|
interface Options {
|
||||||
|
/** Whether the checkbox is checked by default or not */
|
||||||
|
checked?: boolean;
|
||||||
|
/** Whether the checkbox should be disabled or not */
|
||||||
|
disabled?: boolean;
|
||||||
|
/**
|
||||||
|
* The {@link HTMLInputElement.value}/{@link HTMLInputElement.valueAsNumber} associated with the checkbox when checked.
|
||||||
|
*
|
||||||
|
* Defaults to `"on"` if not specified.
|
||||||
|
*/
|
||||||
|
value?: string | number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Rect = { x: number, y: number, w: number, h: number };
|
type Rect = { x: number, y: number, w: number, h: number };
|
||||||
|
|
||||||
/** A 4-tuple with X & Y coordinates, width and height */
|
/** A 4-tuple with X & Y coordinates, width and height */
|
||||||
|
@ -568,6 +585,7 @@ interface PreferenceSubscreen {
|
||||||
run: () => void;
|
run: () => void;
|
||||||
click: () => void;
|
click: () => void;
|
||||||
exit?: () => boolean;
|
exit?: () => boolean;
|
||||||
|
unload?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PreferenceGenderSetting {
|
interface PreferenceGenderSetting {
|
||||||
|
@ -883,7 +901,7 @@ interface IFriendListBeepLogMessage {
|
||||||
MemberName: string;
|
MemberName: string;
|
||||||
ChatRoomName?: string;
|
ChatRoomName?: string;
|
||||||
Private: boolean;
|
Private: boolean;
|
||||||
ChatRoomSpace?: string;
|
ChatRoomSpace?: ServerChatRoomSpace;
|
||||||
Sent: boolean;
|
Sent: boolean;
|
||||||
Time: Date;
|
Time: Date;
|
||||||
Message?: string;
|
Message?: string;
|
||||||
|
@ -1056,6 +1074,11 @@ interface AssetLayer {
|
||||||
readonly GroupAlpha?: readonly Alpha.Data[];
|
readonly GroupAlpha?: readonly Alpha.Data[];
|
||||||
/** @deprecated - Superceded by {@link CreateLayerTypes} */
|
/** @deprecated - Superceded by {@link CreateLayerTypes} */
|
||||||
readonly ModuleType?: never;
|
readonly ModuleType?: never;
|
||||||
|
/**
|
||||||
|
* A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files.
|
||||||
|
*
|
||||||
|
* By default files are expected for _all_ option indices associated with the key(s), unless the valid option set has been narrowed down according to {@link AssetLayer.AllowTypes}.
|
||||||
|
*/
|
||||||
readonly CreateLayerTypes: readonly string[];
|
readonly CreateLayerTypes: readonly string[];
|
||||||
/* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */
|
/* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */
|
||||||
readonly HideForAttribute: readonly AssetAttribute[] | null;
|
readonly HideForAttribute: readonly AssetAttribute[] | null;
|
||||||
|
@ -1213,10 +1236,7 @@ interface Asset {
|
||||||
readonly DynamicScriptDraw: boolean;
|
readonly DynamicScriptDraw: boolean;
|
||||||
/** @deprecated - superceded by {@link CreateLayerTypes} */
|
/** @deprecated - superceded by {@link CreateLayerTypes} */
|
||||||
readonly HasType?: never;
|
readonly HasType?: never;
|
||||||
/**
|
/** A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. */
|
||||||
* A module for which the layer can have types.
|
|
||||||
* Allows one to define different module-specific assets for a single layer.
|
|
||||||
*/
|
|
||||||
readonly CreateLayerTypes: readonly string[];
|
readonly CreateLayerTypes: readonly string[];
|
||||||
/** A record that maps {@link ExtendedItemData.name} to a set with all option indices that support locks */
|
/** A record that maps {@link ExtendedItemData.name} to a set with all option indices that support locks */
|
||||||
readonly AllowLockType: null | Record<string, Set<number>>;
|
readonly AllowLockType: null | Record<string, Set<number>>;
|
||||||
|
@ -2810,7 +2830,7 @@ interface AssetDefinitionProperties {
|
||||||
* A list of allowed activities
|
* A list of allowed activities
|
||||||
* @see {@link Asset.AllowActivity}
|
* @see {@link Asset.AllowActivity}
|
||||||
*/
|
*/
|
||||||
AllowActivity?: string[];
|
AllowActivity?: ActivityName[];
|
||||||
/**
|
/**
|
||||||
* A list of groups allowed activities
|
* A list of groups allowed activities
|
||||||
* @see {@link Asset.AllowActivityOn}
|
* @see {@link Asset.AllowActivityOn}
|
||||||
|
@ -2917,7 +2937,7 @@ interface AssetDefinitionProperties {
|
||||||
type PartialType = `${string}${number}`;
|
type PartialType = `${string}${number}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A record mapping screen names to option indices.
|
* A record mapping extended item data/module names (see {@link ExtendedItemData.Name} and {@link ModularItemModule.Key}) to option indices.
|
||||||
* @see {@link PartialType} A concatenation of a single `TypeRecord` key/value pair.
|
* @see {@link PartialType} A concatenation of a single `TypeRecord` key/value pair.
|
||||||
*/
|
*/
|
||||||
type TypeRecord = Record<string, number>;
|
type TypeRecord = Record<string, number>;
|
||||||
|
@ -4359,6 +4379,8 @@ interface PreferenceExtensionsSettingItem {
|
||||||
* Called when the extension screen is about to exit.
|
* Called when the extension screen is about to exit.
|
||||||
*
|
*
|
||||||
* Happens either through a click of the exit button, or the ESC key.
|
* Happens either through a click of the exit button, or the ESC key.
|
||||||
|
* This will **not** be called if a disconnect happens, so clean up should be
|
||||||
|
* done in {@link PreferenceExtensionsSettingItem.unload}.
|
||||||
*
|
*
|
||||||
* @returns {boolean | void} If you have some validation that needs to happen
|
* @returns {boolean | void} If you have some validation that needs to happen
|
||||||
* (for example, ensuring that a textfield contains a valid color code), return false;
|
* (for example, ensuring that a textfield contains a valid color code), return false;
|
||||||
|
@ -4367,7 +4389,7 @@ interface PreferenceExtensionsSettingItem {
|
||||||
* either through your own means or by setting `PreferenceMessage` to a string.
|
* either through your own means or by setting `PreferenceMessage` to a string.
|
||||||
*
|
*
|
||||||
* If you return true or nothing, your screen's {@link PreferenceExtensionsSettingItem.unload} handler
|
* If you return true or nothing, your screen's {@link PreferenceExtensionsSettingItem.unload} handler
|
||||||
* will be called afterward.
|
* will be called afterward. And the setting screen for the extension will be closed.
|
||||||
*/
|
*/
|
||||||
exit: () => boolean | void;
|
exit: () => boolean | void;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue