@@ -4,6 +4,7 @@ active = [
|
||||
'.sysadmin',
|
||||
'.config',
|
||||
'.user_config',
|
||||
'.skins',
|
||||
'.schedule',
|
||||
'.economy',
|
||||
'.ranks',
|
||||
@@ -20,6 +21,7 @@ active = [
|
||||
'.meta',
|
||||
'.sponsors',
|
||||
'.topgg',
|
||||
'.premium',
|
||||
'.test',
|
||||
]
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Optional
|
||||
|
||||
import discord
|
||||
from core.lion_guild import LionGuild
|
||||
from data.queries import ORDER
|
||||
from meta import LionBot
|
||||
from utils.lib import MessageArgs, jumpto, strfdelta, utc_now
|
||||
from utils.monitor import TaskMonitor
|
||||
@@ -86,7 +87,9 @@ class Ticket:
|
||||
instantiate the correct classes.
|
||||
"""
|
||||
registry: ModerationData = bot.db.registries['ModerationData']
|
||||
rows = await registry.Ticket.fetch_where(*args, **kwargs)
|
||||
rows = await registry.Ticket.fetch_where(*args, **kwargs).order_by(
|
||||
'created_at', ORDER.DESC,
|
||||
)
|
||||
tickets = []
|
||||
if rows:
|
||||
guildids = set(row.guildid for row in rows)
|
||||
|
||||
@@ -46,6 +46,10 @@ async def get_timer_card(bot: LionBot, timer: 'Timer', stage: 'Stage'):
|
||||
else:
|
||||
card_cls = BreakTimerCard
|
||||
|
||||
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||
timer.data.guildid, None, card_cls.card_id
|
||||
)
|
||||
|
||||
return card_cls(
|
||||
name,
|
||||
remaining,
|
||||
|
||||
10
src/modules/premium/__init__.py
Normal file
10
src/modules/premium/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import logging
|
||||
from babel.translator import LocalBabel
|
||||
|
||||
babel = LocalBabel('premium')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
from .cog import PremiumCog
|
||||
await bot.add_cog(PremiumCog(bot))
|
||||
713
src/modules/premium/cog.py
Normal file
713
src/modules/premium/cog.py
Normal file
@@ -0,0 +1,713 @@
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands as cmds
|
||||
import discord.app_commands as appcmds
|
||||
|
||||
from discord.ui.button import Button, ButtonStyle
|
||||
from discord.ui.text_input import TextInput, TextStyle
|
||||
|
||||
from meta import LionCog, LionBot, LionContext
|
||||
from meta.errors import SafeCancellation, UserInputError
|
||||
from meta.logger import log_wrap
|
||||
from utils.lib import utc_now
|
||||
from utils.ui import FastModal
|
||||
from wards import sys_admin_ward
|
||||
from constants import MAX_COINS
|
||||
|
||||
from . import logger, babel
|
||||
from .data import PremiumData, GemTransactionType
|
||||
from .ui.transactions import TransactionList
|
||||
from .ui.premium import PremiumUI
|
||||
from .errors import GemTransactionFailed, BalanceTooLow, BalanceTooHigh
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class PremiumCog(LionCog):
|
||||
buy_gems_link = "https://lionbot.org/donate"
|
||||
|
||||
def __init__(self, bot: LionBot):
|
||||
self.bot = bot
|
||||
self.data: PremiumData = bot.db.load_registry(PremiumData())
|
||||
|
||||
self.gem_logger: Optional[discord.Webhook] = None
|
||||
|
||||
async def cog_load(self):
|
||||
await self.data.init()
|
||||
|
||||
if (leo_setting_cog := self.bot.get_cog('LeoSettings')) is not None:
|
||||
self.crossload_group(self.leo_group, leo_setting_cog.leo_group)
|
||||
|
||||
if (gem_log_url := self.bot.config.endpoints.get('gem_log', None)) is not None:
|
||||
self.gem_logger = discord.Webhook.from_url(gem_log_url, session=self.bot.web_client)
|
||||
|
||||
|
||||
# ----- API -----
|
||||
def buy_gems_button(self) -> Button:
|
||||
t = self.bot.translator.t
|
||||
|
||||
button = Button(
|
||||
style=ButtonStyle.link,
|
||||
label=t(_p(
|
||||
'button:gems|label',
|
||||
"Buy Gems"
|
||||
)),
|
||||
emoji=self.bot.config.emojis.gem,
|
||||
url=self.buy_gems_link,
|
||||
)
|
||||
return button
|
||||
|
||||
async def get_gem_balance(self, userid: int) -> int:
|
||||
"""
|
||||
Get the up-to-date gem balance for this user.
|
||||
|
||||
Creates the User row if it does not already exist.
|
||||
"""
|
||||
record = await self.bot.core.data.User.fetch(userid, cached=False)
|
||||
if record is None:
|
||||
record = await self.bot.core.data.User.create(userid=userid)
|
||||
return record.gems
|
||||
|
||||
async def get_gift_count(self, userid: int) -> int:
|
||||
"""
|
||||
Compute the number of gifts this user has sent, by counting Transaction rows.
|
||||
"""
|
||||
record = await self.data.GemTransaction.table.select_where(
|
||||
from_account=userid,
|
||||
transaction_type=GemTransactionType.GIFT,
|
||||
).select(
|
||||
gift_count='COUNT(*)'
|
||||
).with_no_adapter()
|
||||
|
||||
return record[0]['gift_count'] or 0
|
||||
|
||||
async def is_premium_guild(self, guildid: int) -> bool:
|
||||
"""
|
||||
Check whether the given guild currently has premium status.
|
||||
"""
|
||||
row = await self.data.PremiumGuild.fetch(guildid)
|
||||
now = utc_now()
|
||||
|
||||
premium = (row is not None) and row.premium_until and (row.premium_until > now)
|
||||
return premium
|
||||
|
||||
@log_wrap(isolate=True)
|
||||
async def _add_gems(self, userid: int, amount: int):
|
||||
"""
|
||||
Transaction helper method to atomically add `amount` gems to account `userid`,
|
||||
creating the account if required.
|
||||
|
||||
Do not use this method for a gem transaction. Use `gem_transaction` instead.
|
||||
"""
|
||||
async with self.bot.db.connection() as conn:
|
||||
self.bot.db.conn = conn
|
||||
async with conn.transaction():
|
||||
model = self.bot.core.data.User
|
||||
rows = await model.table.update_where(userid=userid).set(gems=model.gems + amount)
|
||||
if not rows:
|
||||
# User does not exist, create it
|
||||
if amount < 0:
|
||||
raise BalanceTooLow
|
||||
if amount > MAX_COINS:
|
||||
raise BalanceTooHigh
|
||||
row = (await model.create(userid=userid, gems=amount)).data
|
||||
else:
|
||||
row = rows[0]
|
||||
|
||||
if row['gems'] < 0:
|
||||
raise BalanceTooLow
|
||||
|
||||
async def gem_transaction(
|
||||
self,
|
||||
transaction_type: GemTransactionType,
|
||||
*,
|
||||
actorid: int,
|
||||
from_account: Optional[int], to_account: Optional[int],
|
||||
amount: int, description: str,
|
||||
note: Optional[str] = None, reference: Optional[str] = None,
|
||||
) -> PremiumData.GemTransaction:
|
||||
"""
|
||||
Perform a gem transaction with the given parameters.
|
||||
|
||||
This atomically creates a row in the 'gem_transactions' table,
|
||||
updates the account balances,
|
||||
and posts in the gem audit log.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
transaction_type: GemTransactionType
|
||||
The type of transaction.
|
||||
actorid: int
|
||||
The userid of the actor who initiated this transaction.
|
||||
Automatic actions (e.g. webhook triggered) may have their own unique id.
|
||||
from_account: Optional[int]
|
||||
The userid of the source account.
|
||||
May be `None` if there is no source account (e.g. manual modification by admin).
|
||||
to_account: Optional[int]
|
||||
The userid of the destination account.
|
||||
May be `None` if there is no destination account.
|
||||
amount: int
|
||||
The number of LionGems to transfer.
|
||||
description: str
|
||||
An informative description of the transaction for auditing purposes.
|
||||
Should include the pathway (e.g. command) through which the transaction was executed.
|
||||
note: Optional[str]
|
||||
Optional user-readable note added by the actor.
|
||||
Usually attached in a notification visible by the target.
|
||||
(E.g. thanks message from system/admin, or note attached to gift.)
|
||||
reference: str
|
||||
Optional admin-readable transaction reference.
|
||||
This may be the message link of a command message,
|
||||
or an external id/reference for an automatic transaction.
|
||||
|
||||
Raises
|
||||
------
|
||||
BalanceTooLow:
|
||||
Raised if either source or target account would go below 0.
|
||||
"""
|
||||
async with self.bot.db.connection() as conn:
|
||||
self.bot.db.conn = conn
|
||||
async with conn.transaction():
|
||||
if from_account is not None:
|
||||
await self._add_gems(from_account, -amount)
|
||||
if to_account is not None:
|
||||
await self._add_gems(to_account, amount)
|
||||
|
||||
row = await self.data.GemTransaction.create(
|
||||
transaction_type=transaction_type,
|
||||
actorid=actorid,
|
||||
from_account=from_account,
|
||||
to_account=to_account,
|
||||
amount=amount,
|
||||
description=description,
|
||||
note=note,
|
||||
reference=reference,
|
||||
)
|
||||
logger.info(
|
||||
f"LionGem Transaction performed. Transaction data: {row!r}"
|
||||
)
|
||||
await self.audit_log(row)
|
||||
return row
|
||||
|
||||
async def audit_log(self, row: PremiumData.GemTransaction):
|
||||
"""
|
||||
Log the provided gem transaction to the global gem audit log.
|
||||
|
||||
If this fails, or the audit log does not exist, logs a warning.
|
||||
"""
|
||||
posted = False
|
||||
if self.gem_logger is not None:
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.orange(),
|
||||
title=f"Gem Transaction #{row.transactionid}",
|
||||
timestamp=row._timestamp,
|
||||
)
|
||||
embed.add_field(name="Type", value=row.transaction_type.name)
|
||||
embed.add_field(name="Amount", value=str(row.amount))
|
||||
embed.add_field(name="Actor", value=f"<@{row.actorid}>")
|
||||
embed.add_field(name="From Account", value=f"<@{row.from_account}>" if row.from_account else 'None')
|
||||
embed.add_field(name="To Account", value=f"<@{row.to_account}>" if row.to_account else 'None')
|
||||
embed.add_field(name='Description', value=str(row.description), inline=False)
|
||||
if row.note:
|
||||
embed.add_field(name='Note', value=str(row.note), inline=False)
|
||||
if row.reference:
|
||||
embed.add_field(name='Reference', value=str(row.reference), inline=False)
|
||||
|
||||
try:
|
||||
await self.gem_logger.send(embed=embed)
|
||||
posted = True
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
if not posted:
|
||||
logger.warning(
|
||||
f"Missed gem audit logging for gem transaction: {row!r}"
|
||||
)
|
||||
|
||||
# ----- User Commands -----
|
||||
@cmds.hybrid_command(
|
||||
name=_p('cmd:free', "free"),
|
||||
description=_p(
|
||||
'cmd:free|desc',
|
||||
"Get free LionGems!"
|
||||
)
|
||||
)
|
||||
async def cmd_free(self, ctx: LionContext):
|
||||
t = self.bot.translator.t
|
||||
content = t(_p(
|
||||
'cmd:free|embed|description',
|
||||
"You can get free LionGems by sharing our project on your Discord server and social media!\n"
|
||||
"If you have well-established, or YouTube, Instagram, and TikTok accounts,"
|
||||
" we will reward you for creating videos and content about the bot.\n"
|
||||
"If you have a big server, you can promote our project and get LionGems in return.\n"
|
||||
"For more details, contact `arihoresh` or open a Ticket in the [support server](https://discord.gg/studylions)."
|
||||
))
|
||||
thumb = "https://cdn.discordapp.com/attachments/890619584158265405/972791204498530364/Untitled_design_44.png"
|
||||
title = t(_p(
|
||||
'cmd:free|embed|title',
|
||||
"Get FREE LionGems!"
|
||||
))
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
description=content,
|
||||
colour=0x41f097
|
||||
)
|
||||
embed.set_thumbnail(url=thumb)
|
||||
|
||||
await ctx.reply(embed=embed)
|
||||
|
||||
@cmds.hybrid_command(
|
||||
name=_p('cmd:gift', "gift"),
|
||||
description=_p(
|
||||
'cmd:gift|desc',
|
||||
"Gift your LionGems to another user!"
|
||||
)
|
||||
)
|
||||
@appcmds.rename(
|
||||
user=_p('cmd:gift|param:user', "user"),
|
||||
amount=_p('cmd:gift|param:amount', "amount"),
|
||||
note=_p('cmd:gift|param:note', "note"),
|
||||
)
|
||||
@appcmds.describe(
|
||||
user=_p(
|
||||
'cmd:gift|param:user|desc',
|
||||
"User to which you want to gift your LionGems."
|
||||
),
|
||||
amount=_p(
|
||||
'cmd:gift|param:amount|desc',
|
||||
"Number of LionGems to gift."
|
||||
),
|
||||
note=_p(
|
||||
'cmd:gift|param:note|desc',
|
||||
"Optional note to attach to your gift."
|
||||
),
|
||||
)
|
||||
async def cmd_gift(self, ctx: LionContext,
|
||||
user: discord.User,
|
||||
amount: appcmds.Range[int, 1, MAX_COINS],
|
||||
note: Optional[appcmds.Range[str, 1, 1024]] = None):
|
||||
if not ctx.interaction:
|
||||
return
|
||||
t = self.bot.translator.t
|
||||
|
||||
# Validate target
|
||||
if user.bot:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'cmd:gift|error:target_bot',
|
||||
"You cannot gift LionGems to bots!"
|
||||
))
|
||||
)
|
||||
|
||||
if user.id == ctx.author.id:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'cmd:gift|error:target_is_author',
|
||||
"You cannot gift LionGems to yourself!"
|
||||
))
|
||||
)
|
||||
|
||||
# Prepare and open gift confirmation modal
|
||||
amount_field = TextInput(
|
||||
label=t(_p(
|
||||
'cmd:gift|modal:confirm|field:amount|label',
|
||||
"Number of LionGems to Gift"
|
||||
)),
|
||||
default=str(amount),
|
||||
required=True,
|
||||
)
|
||||
note_field = TextInput(
|
||||
label=t(_p(
|
||||
'cmd:gift|modal:confirm|field:note|label',
|
||||
"Add an optional note to your gift"
|
||||
)),
|
||||
default=note or '',
|
||||
required=False,
|
||||
max_length=1024,
|
||||
style=TextStyle.long,
|
||||
)
|
||||
modal = FastModal(
|
||||
amount_field, note_field,
|
||||
title=t(_p(
|
||||
'cmd:gift|modal:confirm|title',
|
||||
"Confirm LionGem Gift"
|
||||
))
|
||||
)
|
||||
|
||||
await ctx.interaction.response.send_modal(modal)
|
||||
|
||||
try:
|
||||
interaction = await modal.wait_for(timeout=300)
|
||||
except asyncio.TimeoutError:
|
||||
# Presume user cancelled and wants to abort
|
||||
raise SafeCancellation
|
||||
|
||||
await interaction.response.defer(thinking=False)
|
||||
|
||||
# Parse amount
|
||||
amountstr = amount_field.value
|
||||
if not amountstr.isdigit():
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'cmd:gift|error:parse_amount',
|
||||
"Could not parse `{provided}` as a number!"
|
||||
)).format(provided=amountstr)
|
||||
)
|
||||
amount = int(amountstr)
|
||||
|
||||
if amount == 0:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'cmd:gift|error:amount_zero',
|
||||
"Cannot gift `0` gems."
|
||||
))
|
||||
)
|
||||
|
||||
# Get author's balance, make sure they have enough
|
||||
author_balance = await self.get_gem_balance(ctx.author.id)
|
||||
if author_balance < amount:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'cmd:gift|error:author_balance_too_low',
|
||||
"Insufficient balance to send {gem}**{amount}**!\n"
|
||||
"Current balance: {gem}**{balance}**"
|
||||
)).format(
|
||||
gem=self.bot.config.emojis.gem,
|
||||
amount=amount,
|
||||
balance=author_balance,
|
||||
)
|
||||
)
|
||||
|
||||
# Everything seems to be in order, run the transaction
|
||||
try:
|
||||
transaction = await self.gem_transaction(
|
||||
GemTransactionType.GIFT,
|
||||
actorid=ctx.author.id,
|
||||
from_account=ctx.author.id, to_account=user.id,
|
||||
amount=amount,
|
||||
description="Gift given through command '/gift'",
|
||||
note=note_field.value or None
|
||||
)
|
||||
except BalanceTooLow:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'cmd:gift|error:balance_too_low',
|
||||
"Insufficient Balance to complete gift!"
|
||||
))
|
||||
)
|
||||
|
||||
# Attempt to send note to user
|
||||
|
||||
thumb = "https://cdn.discordapp.com/attachments/925799205954543636/938704034578194443/C85AF926-9F75-466F-9D8E-D47721427F5D.png"
|
||||
icon = "https://cdn.discordapp.com/attachments/925799205954543636/938703943683416074/4CF1C849-D532-4DEC-B4C9-0AB11F443BAB.png"
|
||||
desc = t(_p(
|
||||
'cmd:gift|target_msg|desc',
|
||||
"You were just gifted {gem}**{amount}** by {user}!\n"
|
||||
"To use them, use the command {skin_cmd} to change your graphics skin!"
|
||||
)).format(
|
||||
gem=self.bot.config.emojis.gem,
|
||||
amount=amount,
|
||||
user=ctx.author.mention,
|
||||
skin_cmd=self.bot.core.mention_cmd('my skin'),
|
||||
)
|
||||
embed = discord.Embed(
|
||||
description=desc,
|
||||
colour=discord.Colour.orange()
|
||||
)
|
||||
embed.set_thumbnail(url=thumb)
|
||||
embed.set_author(
|
||||
name=t(_p('cmd:gift|target_msg|author:name', "LionGems Delivery!")),
|
||||
icon_url=icon,
|
||||
)
|
||||
embed.set_footer(
|
||||
text=t(_p(
|
||||
'cmd:gift|target_msg|footer:text',
|
||||
"You now have {balance} LionGems"
|
||||
)).format(
|
||||
balance=await self.get_gem_balance(user.id),
|
||||
)
|
||||
)
|
||||
embed.timestamp = utc_now()
|
||||
|
||||
note = note_field.value
|
||||
if note:
|
||||
embed.add_field(
|
||||
name=t(_p(
|
||||
'cmd:gift|target_msg|field:note|name',
|
||||
"The sender attached a note"
|
||||
)),
|
||||
value=note
|
||||
)
|
||||
|
||||
notify_sent = False
|
||||
try:
|
||||
await user.send(embed=embed)
|
||||
notify_sent = True
|
||||
except discord.HTTPException:
|
||||
logger.info(
|
||||
f"Could not send LionGem gift target their gift notification. Transaction {transaction.transactionid}"
|
||||
)
|
||||
|
||||
# Finally, send the ack back to the author
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.brand_green(),
|
||||
title=t(_p(
|
||||
'cmd:gift|embed:success|title',
|
||||
"Gift Sent!"
|
||||
)),
|
||||
description=t(_p(
|
||||
'cmd:gift|embed:success|description',
|
||||
"Your gift of {gem}**{amount}** is on its way to {target}!"
|
||||
)).format(
|
||||
gem=self.bot.config.emojis.gem,
|
||||
amount=amount,
|
||||
target=user.mention,
|
||||
)
|
||||
)
|
||||
embed.set_footer(
|
||||
text=t(_p(
|
||||
'cmd:gift|embed:success|footer',
|
||||
"New Balance: {balance} LionGems",
|
||||
)).format(balance=await self.get_gem_balance(ctx.author.id))
|
||||
)
|
||||
if not notify_sent:
|
||||
embed.add_field(
|
||||
name="",
|
||||
value=t(_p(
|
||||
'cmd:gift|embed:success|field:notify_failed|value',
|
||||
"Unfortunately, I couldn't tell them about it! "
|
||||
"They might have direct messages with me turned off."
|
||||
))
|
||||
)
|
||||
|
||||
await ctx.reply(embed=embed, ephemeral=True)
|
||||
|
||||
@cmds.hybrid_command(
|
||||
name=_p('cmd:premium', "premium"),
|
||||
description=_p(
|
||||
'cmd:premium|desc',
|
||||
"Upgrade your server with LionGems!"
|
||||
)
|
||||
)
|
||||
@appcmds.guild_only
|
||||
async def cmd_premium(self, ctx: LionContext):
|
||||
if not ctx.guild:
|
||||
return
|
||||
if not ctx.interaction:
|
||||
return
|
||||
|
||||
ui = PremiumUI(self.bot, ctx.guild, ctx.luser, callerid=ctx.author.id)
|
||||
await ui.run(ctx.interaction)
|
||||
await ui.wait()
|
||||
|
||||
# ----- Owner Commands -----
|
||||
@LionCog.placeholder_group
|
||||
@cmds.hybrid_group("leo", with_app_command=False)
|
||||
async def leo_group(self, ctx: LionContext):
|
||||
...
|
||||
|
||||
|
||||
@leo_group.command(
|
||||
name=_p('cmd:leo_gems', "gems"),
|
||||
description=_p(
|
||||
'cmd:leo_gems|desc',
|
||||
"View and adjust a user's LionGem balance."
|
||||
)
|
||||
)
|
||||
@appcmds.rename(
|
||||
target=_p('cmd:leo_gems|param:target', "target"),
|
||||
adjustment=_p('cmd:leo_gems|param:adjustment', "adjustment"),
|
||||
note=_p('cmd:leo_gems|param:note', "note"),
|
||||
reason=_p('cmd:leo_gems|param:reason', "reason")
|
||||
)
|
||||
@appcmds.describe(
|
||||
target=_p(
|
||||
'cmd:leo_gems|param:target|desc',
|
||||
"Target user you wish to view or modify LionGems for."
|
||||
),
|
||||
adjustment=_p(
|
||||
'cmd:leo_gems|param:adjustment|desc',
|
||||
"Number of LionGems to add to the target's balance (may be negative to remove)"
|
||||
),
|
||||
note=_p(
|
||||
'cmd:leo_gems|param:note|desc',
|
||||
"Optional note to attach to the delivery message when adding LionGems."
|
||||
),
|
||||
reason=_p(
|
||||
'cmd:leo_gems|param:reason|desc',
|
||||
'Optional reason or context to add to the gem audit log for this transaction.'
|
||||
)
|
||||
)
|
||||
@sys_admin_ward
|
||||
async def cmd_leo_gems(self, ctx: LionContext,
|
||||
target: discord.User,
|
||||
adjustment: Optional[int] = None,
|
||||
note: Optional[appcmds.Range[str, 0, 1024]] = None,
|
||||
reason: Optional[appcmds.Range[str, 0, 1024]] = None,):
|
||||
if not ctx.interaction:
|
||||
return
|
||||
t = self.bot.translator.t
|
||||
|
||||
if adjustment is None or adjustment == 0:
|
||||
# History viewing pathway
|
||||
ui = TransactionList(self.bot, target.id, callerid=ctx.author.id)
|
||||
await ui.run(ctx.interaction)
|
||||
await ui.wait()
|
||||
else:
|
||||
# Adjustment path
|
||||
# Show confirmation modal with note and reason
|
||||
adjustment_field = TextInput(
|
||||
label=t(_p(
|
||||
'cmd:leo_gems|adjust|modal:confirm|field:amount|label',
|
||||
"Number of LionGems to add. May be negative."
|
||||
)),
|
||||
default=str(adjustment),
|
||||
required=True,
|
||||
)
|
||||
note_field = TextInput(
|
||||
label=t(_p(
|
||||
'cmd:leo_gems|adjust|modal:confirm|field:note|label',
|
||||
"Optional note to attach to delivery message."
|
||||
)),
|
||||
default=note,
|
||||
style=TextStyle.long,
|
||||
max_length=1024,
|
||||
required=False,
|
||||
)
|
||||
reason_field = TextInput(
|
||||
label=t(_p(
|
||||
'cmd:leo_gems|adjust|modal:confirm|field:reason|label',
|
||||
"Optional reason to add to the audit log."
|
||||
)),
|
||||
default=reason,
|
||||
style=TextStyle.long,
|
||||
max_length=1024,
|
||||
required=False,
|
||||
)
|
||||
|
||||
modal = FastModal(
|
||||
adjustment_field, note_field, reason_field,
|
||||
title=t(_p(
|
||||
'cmd:leo_gems|adjust|modal:confirm|title',
|
||||
"Confirm LionGem Adjustment"
|
||||
))
|
||||
)
|
||||
await ctx.interaction.response.send_modal(modal)
|
||||
|
||||
try:
|
||||
interaction = await modal.wait_for(timeout=300)
|
||||
except asyncio.TimeoutError:
|
||||
raise SafeCancellation
|
||||
|
||||
await interaction.response.defer(thinking=False)
|
||||
|
||||
# Parse values
|
||||
try:
|
||||
amount = int(adjustment_field.value)
|
||||
except ValueError:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'cmd:leo_gems|adjust|error:parse_adjustment',
|
||||
"Could not parse `{given}` as an integer."
|
||||
)).format(given=adjustment_field.value)
|
||||
)
|
||||
note = note_field.value or None
|
||||
reason = reason_field.value or None
|
||||
|
||||
# Run transaction
|
||||
try:
|
||||
transaction = await self.gem_transaction(
|
||||
GemTransactionType.ADMIN,
|
||||
actorid=ctx.author.id,
|
||||
from_account=None, to_account=target.id,
|
||||
amount=amount,
|
||||
description=f"Admin balance adjustment with '/leo gems'.\n{reason}",
|
||||
note=note
|
||||
)
|
||||
except GemTransactionFailed:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'cmd:leo_gems|adjust|error:unknown',
|
||||
"Balance adjustment failed! Check logs for more information."
|
||||
))
|
||||
)
|
||||
# DM user with note if applicable
|
||||
if amount > 0:
|
||||
thumb = "https://cdn.discordapp.com/attachments/925799205954543636/938704034578194443/C85AF926-9F75-466F-9D8E-D47721427F5D.png"
|
||||
icon = "https://cdn.discordapp.com/attachments/925799205954543636/938703943683416074/4CF1C849-D532-4DEC-B4C9-0AB11F443BAB.png"
|
||||
desc = t(_p(
|
||||
'cmd:leo_gems|adjust|target_msg|desc',
|
||||
"You were given {gem}**{amount}**!\n"
|
||||
"To use them, use the command {skin_cmd} to change your graphics skin!"
|
||||
)).format(
|
||||
gem=self.bot.config.emojis.gem,
|
||||
amount=amount,
|
||||
skin_cmd=self.bot.core.mention_cmd('my skin'),
|
||||
)
|
||||
embed = discord.Embed(
|
||||
description=desc,
|
||||
colour=discord.Colour.orange()
|
||||
)
|
||||
embed.set_thumbnail(url=thumb)
|
||||
embed.set_author(
|
||||
name=t(_p('cmd:leo_gems|adjust|target_msg|author:name', "LionGems Delivery!")),
|
||||
icon_url=icon,
|
||||
)
|
||||
embed.set_footer(
|
||||
text=t(_p(
|
||||
'cmd:leo_gems|adjust|target_msg|footer:text',
|
||||
"You now have {balance} LionGems"
|
||||
)).format(
|
||||
balance=await self.get_gem_balance(target.id),
|
||||
)
|
||||
)
|
||||
embed.timestamp = utc_now()
|
||||
|
||||
note = note_field.value
|
||||
if note:
|
||||
embed.add_field(
|
||||
name=t(_p(
|
||||
'cmd:lion_gems|adjust|target_msg|field:note|name',
|
||||
"Note"
|
||||
)),
|
||||
value=note
|
||||
)
|
||||
|
||||
try:
|
||||
await target.send(embed=embed)
|
||||
target_notified = True
|
||||
except discord.HTTPException:
|
||||
target_notified = False
|
||||
else:
|
||||
target_notified = None
|
||||
|
||||
# Ack the operation
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.brand_green(),
|
||||
title=t(_p(
|
||||
'cmd:lion_gems|adjust|embed:success|title',
|
||||
"Success"
|
||||
)),
|
||||
description=t(_p(
|
||||
'cmd:lion_gems|adjust|embed:success|description',
|
||||
"Added {gem}**{amount}** to {target}'s account.\n"
|
||||
"They now have {gem}**{balance}**"
|
||||
)).format(
|
||||
gem=self.bot.config.emojis.gem,
|
||||
target=target.mention,
|
||||
amount=amount,
|
||||
balance=await self.get_gem_balance(target.id),
|
||||
)
|
||||
)
|
||||
if target_notified is False:
|
||||
embed.add_field(
|
||||
name="",
|
||||
value=t(_p(
|
||||
'cmd:lion_gems|adjust|embed:success|field:notify_failed|value',
|
||||
"Could not notify the target, they probably have direct messages disabled."
|
||||
))
|
||||
)
|
||||
|
||||
await ctx.reply(embed=embed, ephemeral=True)
|
||||
92
src/modules/premium/data.py
Normal file
92
src/modules/premium/data.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from enum import Enum
|
||||
|
||||
from psycopg import sql
|
||||
from meta.logger import log_wrap
|
||||
from data import Registry, RowModel, RegisterEnum, Table
|
||||
from data.columns import Integer, Bool, Column, Timestamp, String
|
||||
|
||||
|
||||
class GemTransactionType(Enum):
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
CREATE TYPE GemTransactionType AS ENUM (
|
||||
'ADMIN',
|
||||
'GIFT',
|
||||
'PURCHASE',
|
||||
'AUTOMATIC'
|
||||
);
|
||||
"""
|
||||
ADMIN = 'ADMIN',
|
||||
GIFT = 'GIFT',
|
||||
PURCHASE = 'PURCHASE',
|
||||
AUTOMATIC = 'AUTOMATIC',
|
||||
|
||||
|
||||
class PremiumData(Registry):
|
||||
GemTransactionType = RegisterEnum(GemTransactionType, 'GemTransactionType')
|
||||
|
||||
class GemTransaction(RowModel):
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
|
||||
CREATE TABLE gem_transactions(
|
||||
transactionid SERIAL PRIMARY KEY,
|
||||
transaction_type GemTransactionType NOT NULL,
|
||||
actorid BIGINT NOT NULL,
|
||||
from_account BIGINT,
|
||||
to_account BIGINT,
|
||||
amount INTEGER NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
note TEXT,
|
||||
reference TEXT,
|
||||
_timestamp TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
CREATE INDEX gem_transactions_from ON gem_transactions (from_account);
|
||||
"""
|
||||
_tablename_ = 'gem_transactions'
|
||||
|
||||
transactionid = Integer(primary=True)
|
||||
transaction_type: Column[GemTransactionType] = Column()
|
||||
actorid = Integer()
|
||||
from_account = Integer()
|
||||
to_account = Integer()
|
||||
amount = Integer()
|
||||
description = String()
|
||||
note = String()
|
||||
reference = String()
|
||||
|
||||
_timestamp = Timestamp()
|
||||
|
||||
class PremiumGuild(RowModel):
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
CREATE TABLE premium_guilds(
|
||||
guildid BIGINT PRIMARY KEY REFERENCES guild_config,
|
||||
premium_since TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
premium_until TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
custom_skin_id INTEGER REFERENCES customised_skins
|
||||
);
|
||||
"""
|
||||
_tablename_ = "premium_guilds"
|
||||
_cache_ = {}
|
||||
|
||||
guildid = Integer(primary=True)
|
||||
premium_since = Timestamp()
|
||||
premium_until = Timestamp()
|
||||
custom_skin_id = Integer()
|
||||
|
||||
"""
|
||||
CREATE TABLE premium_guild_contributions(
|
||||
contributionid SERIAL PRIMARY KEY,
|
||||
userid BIGINT NOT NULL REFERENCES user_config,
|
||||
guildid BIGINT NOT NULL REFERENCES premium_guilds,
|
||||
transactionid INTEGER REFERENCES gem_transactions,
|
||||
duration INTEGER NOT NULL,
|
||||
_timestamp TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
"""
|
||||
premium_guild_contributions = Table('premium_guild_contributions')
|
||||
|
||||
19
src/modules/premium/errors.py
Normal file
19
src/modules/premium/errors.py
Normal file
@@ -0,0 +1,19 @@
|
||||
class GemTransactionFailed(Exception):
|
||||
"""
|
||||
Base exception class used when a gem transaction failed.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BalanceTooLow(GemTransactionFailed):
|
||||
"""
|
||||
Exception raised when transaction results in a negative gem balance.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BalanceTooHigh(GemTransactionFailed):
|
||||
"""
|
||||
Exception raised when transaction results in gem balance overflow.
|
||||
"""
|
||||
pass
|
||||
286
src/modules/premium/ui/premium.py
Normal file
286
src/modules/premium/ui/premium.py
Normal file
@@ -0,0 +1,286 @@
|
||||
from typing import Optional, TYPE_CHECKING, NamedTuple
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
|
||||
import discord
|
||||
from discord.ui.button import button, Button, ButtonStyle
|
||||
from psycopg import sql
|
||||
|
||||
from meta import LionBot, conf
|
||||
from meta.logger import log_wrap
|
||||
from core.lion_user import LionUser
|
||||
from babel.translator import LazyStr
|
||||
from meta.errors import ResponseTimedOut, UserInputError
|
||||
from data import RawExpr
|
||||
from modules.premium.errors import BalanceTooLow
|
||||
|
||||
from utils.ui import MessageUI, Confirm, AButton
|
||||
from utils.lib import MessageArgs, utc_now
|
||||
|
||||
from .. import babel, logger
|
||||
from ..data import GemTransactionType, PremiumData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..cog import PremiumCog
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class PremiumPlan(NamedTuple):
|
||||
text: LazyStr
|
||||
label: LazyStr
|
||||
emoji: Optional[discord.PartialEmoji | discord.Emoji | str]
|
||||
duration: int
|
||||
price: int
|
||||
|
||||
|
||||
plans = [
|
||||
PremiumPlan(
|
||||
_p('plan:three_months|text', "three months"),
|
||||
_p('plan:three_months|label', "Three Months"),
|
||||
None,
|
||||
90,
|
||||
4000
|
||||
),
|
||||
PremiumPlan(
|
||||
_p('plan:one_year|text', "one year"),
|
||||
_p('plan:one_year|label', "One Year"),
|
||||
None,
|
||||
365,
|
||||
12000
|
||||
),
|
||||
PremiumPlan(
|
||||
_p('plan:one_month|text', "one month"),
|
||||
_p('plan:one_month|label', "One Month"),
|
||||
None,
|
||||
30,
|
||||
1500
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class PremiumUI(MessageUI):
|
||||
def __init__(self, bot: LionBot, guild: discord.Guild, luser: LionUser, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.bot = bot
|
||||
self.guild = guild
|
||||
self.luser = luser
|
||||
|
||||
self.cog: 'PremiumCog' = bot.get_cog('PremiumCog') # type: ignore
|
||||
|
||||
# UI State
|
||||
self.premium_status: Optional[PremiumData.PremiumGuild] = None
|
||||
|
||||
self.plan_buttons = self._plan_buttons()
|
||||
self.link_button = self.cog.buy_gems_button()
|
||||
|
||||
# ----- API -----
|
||||
# ----- UI Components -----
|
||||
|
||||
async def plan_button(self, press: discord.Interaction, pressed: Button, plan: PremiumPlan):
|
||||
t = self.bot.translator.t
|
||||
|
||||
# Check Balance
|
||||
if self.luser.data.gems < plan.price:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'ui:premium|button:plan|error:insufficient_gems',
|
||||
"You do not have enough LionGems to purchase this plan!"
|
||||
))
|
||||
)
|
||||
|
||||
# Confirm Purchase
|
||||
confirm_msg = t(_p(
|
||||
'ui:premium|button:plan|confirm|desc',
|
||||
"Contributing **{plan_text}** of premium subscription for this server"
|
||||
" will cost you {gem}**{plan_price}**.\n"
|
||||
"Are you sure you want to proceed?"
|
||||
)).format(
|
||||
plan_text=t(plan.text),
|
||||
gem=self.bot.config.emojis.gem,
|
||||
plan_price=plan.price,
|
||||
)
|
||||
confirm = Confirm(confirm_msg, press.user.id)
|
||||
confirm.embed.title = t(_p(
|
||||
'ui:premium|button:plan|confirm|title',
|
||||
"Confirm Server Upgrade"
|
||||
))
|
||||
confirm.embed.set_footer(
|
||||
text=t(_p(
|
||||
'ui:premium|button:plan|confirm|footer',
|
||||
"Your current balance is {balance} LionGems"
|
||||
)).format(balance=self.luser.data.gems)
|
||||
)
|
||||
confirm.embed.colour = 0x41f097
|
||||
|
||||
try:
|
||||
result = await confirm.ask(press, ephemeral=True)
|
||||
except ResponseTimedOut:
|
||||
result = False
|
||||
if not result:
|
||||
await press.followup.send(
|
||||
t(_p(
|
||||
'ui:premium|button:plan|confirm|cancelled',
|
||||
"Purchase cancelled! No LionGems were deducted from your account."
|
||||
)),
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Write transaction, plan contribution, and new plan status, with potential rollback
|
||||
try:
|
||||
await self._do_premium_upgrade(plan)
|
||||
except BalanceTooLow:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'ui:premium|button:plan|error:insufficient_gems_post_confirm',
|
||||
"Insufficient LionGems to purchase this plan!"
|
||||
))
|
||||
)
|
||||
|
||||
# Acknowledge premium
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.brand_green(),
|
||||
title=t(_p(
|
||||
'ui:premium|button:plan|embed:success|title',
|
||||
"Server Upgraded!"
|
||||
)),
|
||||
description=t(_p(
|
||||
'ui:premium|button:plan|embed:success|desc',
|
||||
"You have contributed **{plan_text}** of premium subscription to this server!"
|
||||
)).format(plan_text=plan.text)
|
||||
)
|
||||
await press.followup.send(
|
||||
embed=embed
|
||||
)
|
||||
await self.refresh()
|
||||
|
||||
@log_wrap(action='premium upgrade')
|
||||
async def _do_premium_upgrade(self, plan: PremiumPlan):
|
||||
async with self.bot.db.connection() as conn:
|
||||
self.bot.db.conn = conn
|
||||
async with conn.transaction():
|
||||
# Perform the gem transaction
|
||||
transaction = await self.cog.gem_transaction(
|
||||
GemTransactionType.PURCHASE,
|
||||
actorid=self.luser.userid,
|
||||
from_account=self.luser.userid,
|
||||
to_account=None,
|
||||
amount=plan.price,
|
||||
description=(
|
||||
f"User purchased {plan.duration} days of premium"
|
||||
f" for guild {self.guild.id} using the `PremiumUI`."
|
||||
),
|
||||
note=None,
|
||||
reference=f"iid: {self._original.id if self._original else 'None'}"
|
||||
)
|
||||
|
||||
model = self.cog.data.PremiumGuild
|
||||
# Ensure the premium guild row exists
|
||||
premium_guild = await model.fetch_or_create(self.guild.id)
|
||||
|
||||
# Extend the subscription
|
||||
await model.table.update_where(guildid=self.guild.id).set(
|
||||
premium_until=RawExpr(
|
||||
sql.SQL("GREATEST(premium_until, now()) + {}").format(
|
||||
sql.Placeholder()
|
||||
),
|
||||
(dt.timedelta(days=plan.duration),)
|
||||
)
|
||||
)
|
||||
|
||||
# Finally, record the user's contribution
|
||||
await self.cog.data.premium_guild_contributions.insert(
|
||||
userid=self.luser.userid, guildid=self.guild.id,
|
||||
transactionid=transaction.transactionid, duration=plan.duration
|
||||
)
|
||||
|
||||
def _plan_buttons(self) -> list[Button]:
|
||||
"""
|
||||
Generate the Plan buttons.
|
||||
|
||||
Intended to be used once, upon initialisation.
|
||||
"""
|
||||
t = self.bot.translator.t
|
||||
buttons = []
|
||||
for plan in plans:
|
||||
butt = AButton(
|
||||
label=t(plan.label),
|
||||
emoji=plan.emoji,
|
||||
style=ButtonStyle.blurple,
|
||||
pass_kwargs={'plan': plan}
|
||||
)
|
||||
butt(self.plan_button)
|
||||
self.add_item(butt)
|
||||
buttons.append(butt)
|
||||
return buttons
|
||||
|
||||
# ----- UI Flow -----
|
||||
def _current_status(self) -> str:
|
||||
t = self.bot.translator.t
|
||||
|
||||
if self.premium_status is None or self.premium_status.premium_until is None:
|
||||
status = t(_p(
|
||||
'ui:premium|current_status:none',
|
||||
"__**Current Server Status:**__ Awaiting Upgrade."
|
||||
))
|
||||
elif self.premium_status.premium_until > utc_now():
|
||||
status = t(_p(
|
||||
'ui:premium|current_status:premium',
|
||||
"__**Current Server Status:**__ Upgraded! Premium until {expiry}"
|
||||
)).format(expiry=discord.utils.format_dt(self.premium_status.premium_until, 'd'))
|
||||
else:
|
||||
status = t(_p(
|
||||
'ui:premium|current_status:none',
|
||||
"__**Current Server Status:**__ Awaiting Upgrade. Premium status expired on {expiry}"
|
||||
)).format(expiry=discord.utils.format_dt(self.premium_status.premium_until, 'd'))
|
||||
|
||||
return status
|
||||
|
||||
async def make_message(self) -> MessageArgs:
|
||||
t = self.bot.translator.t
|
||||
|
||||
blurb = t(_p(
|
||||
'ui:premium|embed|description',
|
||||
"By supporting our project, you will get access to countless customisation features!\n\n"
|
||||
"- **Rebranding:** Customizable HEX colours and"
|
||||
" **beautiful premium skins** for all of your community members!\n"
|
||||
"- **Remove the vote and sponsor prompt!**\n"
|
||||
"- Access to all of the [future premium features](https://staging.lionbot.org/donate)\n\n"
|
||||
"Both server owners and **regular users** can"
|
||||
" **buy and gift a subscription for this server** using this command!\n"
|
||||
"To support both Leo and your server, **use the buttons below**!"
|
||||
)) + '\n\n' + self._current_status()
|
||||
|
||||
embed = discord.Embed(
|
||||
colour=0x41f097,
|
||||
title=t(_p(
|
||||
'ui:premium|embed|title',
|
||||
"Support Leo and Upgrade your Server!"
|
||||
)),
|
||||
description=blurb,
|
||||
)
|
||||
embed.set_thumbnail(
|
||||
url="https://i.imgur.com/v1mZolL.png"
|
||||
)
|
||||
embed.set_image(
|
||||
url="https://cdn.discordapp.com/attachments/824196406482305034/972405513570615326/premium_test.png"
|
||||
)
|
||||
embed.set_footer(
|
||||
text=t(_p(
|
||||
'ui:premium|embed|footer',
|
||||
"Your current balance is {balance} LionGems."
|
||||
)).format(balance=self.luser.data.gems)
|
||||
)
|
||||
|
||||
return MessageArgs(embed=embed)
|
||||
|
||||
async def refresh_layout(self):
|
||||
self.set_layout(
|
||||
(*self.plan_buttons, self.link_button),
|
||||
)
|
||||
|
||||
async def reload(self):
|
||||
self.premium_status = await self.cog.data.PremiumGuild.fetch(self.guild.id, cached=False)
|
||||
await self.luser.data.refresh()
|
||||
198
src/modules/premium/ui/transactions.py
Normal file
198
src/modules/premium/ui/transactions.py
Normal file
@@ -0,0 +1,198 @@
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
|
||||
import discord
|
||||
from discord.ui.button import button, Button, ButtonStyle
|
||||
|
||||
from meta import LionBot, conf
|
||||
from data import ORDER
|
||||
|
||||
from utils.ui import MessageUI, input
|
||||
from utils.lib import MessageArgs, tabulate
|
||||
|
||||
from .. import babel, logger
|
||||
from ..data import PremiumData
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class TransactionList(MessageUI):
|
||||
block_len = 5
|
||||
|
||||
def __init__(self, bot: LionBot, userid: int, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.bot = bot
|
||||
self.userid = userid
|
||||
|
||||
self._pagen = 0
|
||||
self.blocks: list[list[PremiumData.GemTransaction]] = [[]]
|
||||
|
||||
@property
|
||||
def page_count(self):
|
||||
return len(self.blocks)
|
||||
|
||||
@property
|
||||
def pagen(self):
|
||||
self._pagen = self._pagen % self.page_count
|
||||
return self._pagen
|
||||
|
||||
@pagen.setter
|
||||
def pagen(self, value):
|
||||
self._pagen = value % self.page_count
|
||||
|
||||
@property
|
||||
def current_page(self):
|
||||
return self.blocks[self.pagen]
|
||||
|
||||
# ----- UI Components -----
|
||||
|
||||
# Backwards
|
||||
@button(emoji=conf.emojis.backward, style=ButtonStyle.grey)
|
||||
async def prev_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
self.pagen -= 1
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
# Jump to page
|
||||
@button(label="JUMP_PLACEHOLDER", style=ButtonStyle.blurple)
|
||||
async def jump_button(self, press: discord.Interaction, pressed: Button):
|
||||
"""
|
||||
Jump-to-page button.
|
||||
Loads a page-switch dialogue.
|
||||
"""
|
||||
t = self.bot.translator.t
|
||||
try:
|
||||
interaction, value = await input(
|
||||
press,
|
||||
title=t(_p(
|
||||
'ui:transactions|button:jump|input:title',
|
||||
"Jump to page"
|
||||
)),
|
||||
question=t(_p(
|
||||
'ui:transactions|button:jump|input:question',
|
||||
"Page number to jump to"
|
||||
))
|
||||
)
|
||||
value = value.strip()
|
||||
except asyncio.TimeoutError:
|
||||
return
|
||||
|
||||
if not value.lstrip('- ').isdigit():
|
||||
error_embed = discord.Embed(
|
||||
title=t(_p(
|
||||
'ui:transactions|button:jump|error:invalid_page',
|
||||
"Invalid page number, please try again!"
|
||||
)),
|
||||
colour=discord.Colour.brand_red()
|
||||
)
|
||||
await interaction.response.send_message(embed=error_embed, ephemeral=True)
|
||||
else:
|
||||
await interaction.response.defer(thinking=True)
|
||||
pagen = int(value.lstrip('- '))
|
||||
if value.startswith('-'):
|
||||
pagen = -1 * pagen
|
||||
elif pagen > 0:
|
||||
pagen = pagen - 1
|
||||
self.pagen = pagen
|
||||
await self.refresh(thinking=interaction)
|
||||
|
||||
async def jump_button_refresh(self):
|
||||
component = self.jump_button
|
||||
component.label = f"{self.pagen + 1}/{self.page_count}"
|
||||
component.disabled = (self.page_count <= 1)
|
||||
|
||||
# Forward
|
||||
@button(emoji=conf.emojis.forward, style=ButtonStyle.grey)
|
||||
async def next_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True)
|
||||
self.pagen += 1
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
# Quit
|
||||
@button(emoji=conf.emojis.cancel, style=ButtonStyle.red)
|
||||
async def quit_button(self, press: discord.Interaction, pressed: Button):
|
||||
"""
|
||||
Quit the UI.
|
||||
"""
|
||||
await press.response.defer()
|
||||
await self.quit()
|
||||
|
||||
# ----- UI Flow -----
|
||||
async def make_message(self) -> MessageArgs:
|
||||
t = self.bot.translator.t
|
||||
|
||||
title = t(_p(
|
||||
'ui:transactions|embed|title',
|
||||
"Gem Transactions for user `{userid}`"
|
||||
)).format(userid=self.userid)
|
||||
|
||||
rows = self.current_page
|
||||
if rows:
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.orange(),
|
||||
title=title,
|
||||
description=t(_p(
|
||||
'ui:transactions|embed|desc:balance',
|
||||
"User {target} has a LionGem balance of {gem}**{balance}**"
|
||||
)).format(
|
||||
gem=self.bot.config.emojis.gem,
|
||||
target=f"<@{self.userid}>",
|
||||
balance=await (self.bot.get_cog('PremiumCog')).get_gem_balance(self.userid),
|
||||
)
|
||||
)
|
||||
for row in rows:
|
||||
name = f"Transaction #{row.transactionid}"
|
||||
table_rows = (
|
||||
('timestamp', discord.utils.format_dt(row._timestamp)),
|
||||
('type', row.transaction_type.name),
|
||||
('amount', str(row.amount)),
|
||||
('actor', f"<@{row.actorid}>"),
|
||||
('from', f"`{row.from_account}`" if row.from_account else 'None'),
|
||||
('to', f"`{row.to_account}`" if row.to_account else 'None'),
|
||||
('reference', str(row.reference)),
|
||||
)
|
||||
table = '\n'.join(tabulate(*table_rows))
|
||||
embed.add_field(
|
||||
name=name,
|
||||
value=f"{row.description}\n{table}",
|
||||
inline=False
|
||||
)
|
||||
else:
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.brand_red(),
|
||||
description = t(_p(
|
||||
'ui:transactions|embed|desc:no_transactions',
|
||||
"This user has no related gem transactions!"
|
||||
))
|
||||
)
|
||||
return MessageArgs(embed=embed)
|
||||
|
||||
async def refresh_layout(self):
|
||||
to_refresh = (
|
||||
self.jump_button_refresh(),
|
||||
)
|
||||
await asyncio.gather(*to_refresh)
|
||||
|
||||
if self.page_count > 1:
|
||||
self.set_layout(
|
||||
(self.prev_button, self.jump_button, self.quit_button, self.next_button),
|
||||
)
|
||||
else:
|
||||
self.set_layout(
|
||||
(self.quit_button,)
|
||||
)
|
||||
|
||||
async def reload(self):
|
||||
model = PremiumData.GemTransaction
|
||||
|
||||
rows = await model.fetch_where(
|
||||
(model.from_account == self.userid) | (model.to_account == self.userid)
|
||||
).order_by('_timestamp', ORDER.DESC)
|
||||
|
||||
blocks = [
|
||||
rows[i:i+self.block_len]
|
||||
for i in range(0, len(rows), self.block_len)
|
||||
]
|
||||
self.blocks = blocks or [[]]
|
||||
@@ -592,20 +592,25 @@ class RankCog(LionCog):
|
||||
# Calculate destination
|
||||
to_dm = lguild.config.get('dm_ranks').value
|
||||
rank_channel = lguild.config.get('rank_channel').value
|
||||
sent = False
|
||||
|
||||
if to_dm or not rank_channel:
|
||||
if to_dm:
|
||||
destination = member
|
||||
embed.set_author(
|
||||
name=guild.name,
|
||||
icon_url=guild.icon.url if guild.icon else None
|
||||
)
|
||||
text = None
|
||||
else:
|
||||
try:
|
||||
await destination.send(embed=embed)
|
||||
sent = True
|
||||
except discord.HTTPException:
|
||||
if not rank_channel:
|
||||
raise
|
||||
|
||||
if not sent and rank_channel:
|
||||
destination = rank_channel
|
||||
text = member.mention
|
||||
|
||||
# Post!
|
||||
await destination.send(embed=embed, content=text)
|
||||
await destination.send(content=text, embed=embed)
|
||||
|
||||
def get_message_map(self,
|
||||
rank_type: RankType,
|
||||
|
||||
10
src/modules/skins/__init__.py
Normal file
10
src/modules/skins/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import logging
|
||||
from babel.translator import LocalBabel
|
||||
|
||||
babel = LocalBabel('customskins')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
from .cog import CustomSkinCog
|
||||
await bot.add_cog(CustomSkinCog(bot))
|
||||
388
src/modules/skins/cog.py
Normal file
388
src/modules/skins/cog.py
Normal file
@@ -0,0 +1,388 @@
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands as cmds
|
||||
import discord.app_commands as appcmds
|
||||
from cachetools import LRUCache
|
||||
from bidict import bidict
|
||||
from frozendict import frozendict
|
||||
|
||||
|
||||
from meta import LionCog, LionBot, LionContext
|
||||
from meta.errors import UserInputError
|
||||
from meta.logger import log_wrap
|
||||
from utils.lib import MISSING, utc_now
|
||||
from wards import sys_admin_ward, low_management_ward
|
||||
from gui.base import AppSkin
|
||||
from babel.translator import ctx_locale
|
||||
|
||||
from . import logger, babel
|
||||
from .data import CustomSkinData
|
||||
from .skinlib import appskin_as_choice, FrozenCustomSkin, CustomSkin
|
||||
from .settings import GlobalSkinSettings
|
||||
from .settingui import GlobalSkinSettingUI
|
||||
from .userskinui import UserSkinUI
|
||||
from .editor.skineditor import CustomSkinEditor
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class CustomSkinCog(LionCog):
|
||||
def __init__(self, bot: LionBot):
|
||||
self.bot = bot
|
||||
self.data: CustomSkinData = bot.db.load_registry(CustomSkinData())
|
||||
self.bot_settings = GlobalSkinSettings()
|
||||
|
||||
# Cache of app skin id -> app skin name
|
||||
# After initialisation, contains all the base skins available for this app
|
||||
self.appskin_names: bidict[int, str] = bidict()
|
||||
|
||||
# Bijective cache of skin property ids <-> (card_id, property_name) tuples
|
||||
self.skin_properties: bidict[int, tuple[str, str]] = bidict()
|
||||
|
||||
# Cache of currently active user skins
|
||||
# Invalidation handled by local event handler
|
||||
self.active_user_skinids: LRUCache[int, Optional[int]] = LRUCache(maxsize=5000)
|
||||
|
||||
# Cache of custom skin id -> frozen custom skin
|
||||
self.custom_skins: LRUCache[int, FrozenCustomSkin] = LRUCache(maxsize=1000)
|
||||
|
||||
self.current_default: Optional[str] = None
|
||||
|
||||
async def cog_load(self):
|
||||
await self.data.init()
|
||||
|
||||
if (leo_setting_cog := self.bot.get_cog('LeoSettings')) is not None:
|
||||
leo_setting_cog.bot_setting_groups.append(self.bot_settings)
|
||||
self.crossload_group(self.leo_group, leo_setting_cog.leo_group)
|
||||
|
||||
if (config_cog := self.bot.get_cog('ConfigCog')) is not None:
|
||||
self.crossload_group(self.admin_group, config_cog.admin_group)
|
||||
|
||||
if (user_cog := self.bot.get_cog('UserConfigCog')) is not None:
|
||||
self.crossload_group(self.my_group, user_cog.userconfig_group)
|
||||
|
||||
await self._reload_appskins()
|
||||
await self._reload_property_map()
|
||||
await self.get_default_skin()
|
||||
|
||||
async def _reload_property_map(self):
|
||||
"""
|
||||
Reload the skin property id to (card_id, property_name) bijection.
|
||||
"""
|
||||
records = await self.data.skin_property_map.select_where()
|
||||
cache = self.skin_properties
|
||||
|
||||
cache.clear()
|
||||
for record in records:
|
||||
cache[record['property_id']] = (record['card_id'], record['property_name'])
|
||||
|
||||
logger.info(
|
||||
f"Loaded '{len(cache)}' custom skin properties."
|
||||
)
|
||||
|
||||
async def _reload_appskins(self):
|
||||
"""
|
||||
Reload the global_available_skin id to the appskin name.
|
||||
Create global_available_skins that don't already exist.
|
||||
"""
|
||||
cache = self.appskin_names
|
||||
available = list(AppSkin.skins_data['skin_map'].keys())
|
||||
rows = await self.data.GlobalSkin.fetch_where(skin_name=available)
|
||||
|
||||
cache.clear()
|
||||
for row in rows:
|
||||
cache[row.skin_id] = row.skin_name
|
||||
|
||||
# Not caring about efficiency here because this essentially needs to happen once ever
|
||||
missing = [name for name in available if name not in cache.values()]
|
||||
for name in missing:
|
||||
row = await self.data.GlobalSkin.create(skin_name=name)
|
||||
cache[row.skin_id] = row.skin_name
|
||||
|
||||
logger.info(
|
||||
f"Loaded '{len(cache)}' global base skins."
|
||||
)
|
||||
|
||||
# ----- Internal API -----
|
||||
def get_base(self, base_skin_id: int) -> AppSkin:
|
||||
"""
|
||||
Initialise a localised AppSkin for the given base skin id.
|
||||
"""
|
||||
if base_skin_id not in self.appskin_names:
|
||||
raise ValueError(f"Unknown app skin id '{base_skin_id}'")
|
||||
|
||||
return AppSkin.get(
|
||||
skin_id=self.appskin_names[base_skin_id],
|
||||
locale=ctx_locale.get(),
|
||||
use_cache=True,
|
||||
)
|
||||
|
||||
async def get_default_skin(self) -> Optional[str]:
|
||||
"""
|
||||
Get the current app-default skin, and return it as a skin name.
|
||||
|
||||
May be None if there is no app-default set.
|
||||
This should almost always hit cache.
|
||||
"""
|
||||
setting = self.bot_settings.DefaultSkin
|
||||
instance = await setting.get(self.bot.appname)
|
||||
self.current_default = instance.value
|
||||
return instance.value
|
||||
|
||||
async def fetch_property_ids(self, *card_properties: tuple[str, str]) -> list[int]:
|
||||
"""
|
||||
Fetch the skin property ids for the given (card_id, property_name) tuples.
|
||||
|
||||
Creates any missing properties.
|
||||
"""
|
||||
mapper = self.skin_properties.inverse
|
||||
missing = [prop for prop in card_properties if prop not in mapper]
|
||||
if missing:
|
||||
# First insert missing properties
|
||||
await self.data.skin_property_map.insert_many(
|
||||
('card_id', 'property_name'),
|
||||
*missing
|
||||
)
|
||||
await self._reload_property_map()
|
||||
return [mapper[prop] for prop in card_properties]
|
||||
|
||||
async def get_guild_skinid(self, guildid: int) -> Optional[int]:
|
||||
"""
|
||||
Fetch the custom_skin_id associated to the current guild.
|
||||
|
||||
Returns None if the guild is not premium or has no custom skin set.
|
||||
Usually hits cache (Specifically the PremiumGuild cache).
|
||||
"""
|
||||
cog = self.bot.get_cog('PremiumCog')
|
||||
if not cog:
|
||||
logger.error(
|
||||
"Trying to get guild skinid without loaded premium cog!"
|
||||
)
|
||||
return None
|
||||
row = await cog.data.PremiumGuild.fetch(guildid)
|
||||
return row.custom_skin_id if row else None
|
||||
|
||||
async def get_user_skinid(self, userid: int) -> Optional[int]:
|
||||
"""
|
||||
Fetch the custom_skin_id of the active skin in the given user's skin inventory.
|
||||
|
||||
Returns None if the user does not have an active skin.
|
||||
Should usually be cached by `self.active_user_skinids`.
|
||||
"""
|
||||
skinid = self.active_user_skinids.get(userid, MISSING)
|
||||
if skinid is MISSING:
|
||||
rows = await self.data.UserSkin.fetch_where(userid=userid, active=True)
|
||||
skinid = rows[0].custom_skin_id if rows else None
|
||||
self.active_user_skinids[userid] = skinid
|
||||
return skinid
|
||||
|
||||
async def args_for_skin(self, skinid: int, cardid: str) -> dict[str, str]:
|
||||
"""
|
||||
Fetch the skin argument dictionary for the given custom_skin_id.
|
||||
|
||||
Should usually be cached by `self.custom_skin_args`.
|
||||
"""
|
||||
skin = self.custom_skins.get(skinid, None)
|
||||
if skin is None:
|
||||
custom_skin = await CustomSkin.fetch(self.bot, skinid)
|
||||
skin = custom_skin.freeze()
|
||||
self.custom_skins[skinid] = skin
|
||||
return skin.args_for(cardid)
|
||||
|
||||
# ----- External API -----
|
||||
async def get_skinargs_for(self,
|
||||
guildid: Optional[int], userid: Optional[int], card_id: str
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Get skin arguments for a standard GUI render with the given guild, user, and for the given card.
|
||||
|
||||
Takes into account the global defaults, guild custom skin, and user active skin.
|
||||
"""
|
||||
args = {}
|
||||
|
||||
if userid and (skinid := await self.get_user_skinid(userid)):
|
||||
skin_args = await self.args_for_skin(skinid, card_id)
|
||||
args.update(skin_args)
|
||||
elif guildid and (skinid := await self.get_guild_skinid(guildid)):
|
||||
skin_args = await self.args_for_skin(skinid, card_id)
|
||||
args.update(skin_args)
|
||||
|
||||
default = self.current_default
|
||||
if default:
|
||||
args.setdefault("base_skin_id", default)
|
||||
|
||||
return args
|
||||
|
||||
# ----- Event Handlers -----
|
||||
@LionCog.listener('on_userset_skin')
|
||||
async def refresh_user_skin(self, userid: int):
|
||||
"""
|
||||
Update cached user active skinid.
|
||||
"""
|
||||
self.active_user_skinids.pop(userid, None)
|
||||
await self.get_user_skinid(userid)
|
||||
|
||||
@LionCog.listener('on_skin_updated')
|
||||
async def refresh_custom_skin(self, skinid: int):
|
||||
"""
|
||||
Update cached args for given custom skin id.
|
||||
"""
|
||||
self.custom_skins.pop(skinid, None)
|
||||
custom_skin = await CustomSkin.fetch(self.bot, skinid)
|
||||
if custom_skin is not None:
|
||||
skin = custom_skin.freeze()
|
||||
self.custom_skins[skinid] = skin
|
||||
|
||||
@LionCog.listener('on_botset_skin')
|
||||
async def handle_botset_skin(self, appname, instance):
|
||||
await self.bot.global_dispatch('global_botset_skin', appname)
|
||||
|
||||
@LionCog.listener('on_global_botset_skin')
|
||||
async def refresh_default_skin(self, appname):
|
||||
await self.bot.core.data.BotConfig.fetch(appname, cached=False)
|
||||
await self.get_default_skin()
|
||||
|
||||
# ----- Userspace commands -----
|
||||
@LionCog.placeholder_group
|
||||
@cmds.hybrid_group("my", with_app_command=False)
|
||||
async def my_group(self, ctx: LionContext):
|
||||
...
|
||||
|
||||
@my_group.command(
|
||||
name=_p('cmd:my_skin', "skin"),
|
||||
description=_p(
|
||||
'cmd:my_skin|desc',
|
||||
"Change the colours of your interface"
|
||||
)
|
||||
)
|
||||
async def cmd_my_skin(self, ctx: LionContext):
|
||||
if not ctx.interaction:
|
||||
return
|
||||
ui = UserSkinUI(self.bot, ctx.author.id, ctx.author.id)
|
||||
await ui.run(ctx.interaction, ephemeral=True)
|
||||
await ui.wait()
|
||||
|
||||
# ----- Adminspace commands -----
|
||||
@LionCog.placeholder_group
|
||||
@cmds.hybrid_group("admin", with_app_command=False)
|
||||
async def admin_group(self, ctx: LionContext):
|
||||
...
|
||||
|
||||
@admin_group.command(
|
||||
name=_p('cmd:admin_brand', "brand"),
|
||||
description=_p(
|
||||
'cmd:admin_brand|desc',
|
||||
"Fully customise my default interface for your members!"
|
||||
)
|
||||
)
|
||||
@low_management_ward
|
||||
async def cmd_admin_brand(self, ctx: LionContext):
|
||||
if not ctx.interaction:
|
||||
return
|
||||
if not ctx.guild:
|
||||
return
|
||||
t = self.bot.translator.t
|
||||
|
||||
# Check guild premium status
|
||||
premiumcog = self.bot.get_cog('PremiumCog')
|
||||
guild_row = await premiumcog.data.PremiumGuild.fetch(ctx.guild.id, cached=False)
|
||||
|
||||
if not guild_row:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'cmd:admin_brand|error:not_premium',
|
||||
"Only premium servers can modify their interface theme! "
|
||||
"Use the {premium} command to upgrade your server."
|
||||
)).format(premium=self.bot.core.mention_cmd('premium'))
|
||||
)
|
||||
|
||||
await ctx.interaction.response.defer(thinking=True, ephemeral=False)
|
||||
|
||||
if guild_row.custom_skin_id is None:
|
||||
# Create new custom skin
|
||||
skin_data = await self.data.CustomisedSkin.create(
|
||||
base_skin_id=self.appskin_names.inverse[self.current_default] if self.current_default else None
|
||||
)
|
||||
await guild_row.update(custom_skin_id=skin_data.custom_skin_id)
|
||||
|
||||
skinid = guild_row.custom_skin_id
|
||||
custom_skin = await CustomSkin.fetch(self.bot, skinid)
|
||||
if custom_skin is None:
|
||||
raise ValueError("Invalid custom skin id")
|
||||
|
||||
# Open the CustomSkinEditor with this skin
|
||||
ui = CustomSkinEditor(custom_skin, callerid=ctx.author.id)
|
||||
await ui.send(ctx.channel)
|
||||
await ctx.interaction.delete_original_response()
|
||||
await ui.wait()
|
||||
|
||||
# ----- Owner commands -----
|
||||
@LionCog.placeholder_group
|
||||
@cmds.hybrid_group("leo", with_app_command=False)
|
||||
async def leo_group(self, ctx: LionContext):
|
||||
...
|
||||
|
||||
@leo_group.command(
|
||||
name=_p('cmd:leo_skin', "skin"),
|
||||
description=_p(
|
||||
'cmd:leo_skin|desc',
|
||||
"View and update the global skin settings"
|
||||
)
|
||||
)
|
||||
@appcmds.rename(
|
||||
default_skin=_p('cmd:leo_skin|param:default_skin', "default_skin"),
|
||||
)
|
||||
@appcmds.describe(
|
||||
default_skin=_p(
|
||||
'cmd:leo_skin|param:default_skin|desc',
|
||||
"Set the global default skin."
|
||||
)
|
||||
)
|
||||
@sys_admin_ward
|
||||
async def cmd_leo_skin(self, ctx: LionContext,
|
||||
default_skin: Optional[str] = None):
|
||||
if not ctx.interaction:
|
||||
return
|
||||
|
||||
await ctx.interaction.response.defer(thinking=True)
|
||||
modified = []
|
||||
|
||||
if default_skin is not None:
|
||||
setting = self.bot_settings.DefaultSkin
|
||||
instance = await setting.from_string(self.bot.appname, default_skin)
|
||||
modified.append(instance)
|
||||
|
||||
for instance in modified:
|
||||
await instance.write()
|
||||
|
||||
# No update_str, just show the config window
|
||||
ui = GlobalSkinSettingUI(self.bot, self.bot.appname, ctx.channel.id)
|
||||
await ui.run(ctx.interaction)
|
||||
await ui.wait()
|
||||
|
||||
|
||||
@cmd_leo_skin.autocomplete('default_skin')
|
||||
async def cmd_leo_skin_acmpl_default_skin(self, interaction: discord.Interaction, partial: str):
|
||||
babel = self.bot.get_cog('BabelCog')
|
||||
ctx_locale.set(await babel.get_user_locale(interaction.user.id))
|
||||
|
||||
choices = []
|
||||
for skinid in self.appskin_names:
|
||||
appskin = self.get_base(skinid)
|
||||
match = partial.lower()
|
||||
if match in appskin.skin_id.lower() or match in appskin.display_name.lower():
|
||||
choices.append(appskin_as_choice(appskin))
|
||||
if not choices:
|
||||
t = self.bot.translator.t
|
||||
choices = [
|
||||
appcmds.Choice(
|
||||
name=t(_p(
|
||||
'cmd:leo_skin|acmpl:default_skin|error:no_match',
|
||||
"No app skins matching {partial}"
|
||||
)).format(partial=partial)[:100],
|
||||
value=partial
|
||||
)
|
||||
]
|
||||
return choices
|
||||
117
src/modules/skins/data.py
Normal file
117
src/modules/skins/data.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from data import Registry, RowModel, Table
|
||||
from data.columns import Integer, Bool, Timestamp, String
|
||||
|
||||
|
||||
class CustomSkinData(Registry):
|
||||
class GlobalSkin(RowModel):
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
CREATE TABLE global_available_skins(
|
||||
skin_id SERIAL PRIMARY KEY,
|
||||
skin_name TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX global_available_skin_names ON global_available_skins (skin_name);
|
||||
"""
|
||||
_tablename_ = 'global_available_skins'
|
||||
_cache_ = {}
|
||||
|
||||
skin_id = Integer(primary=True)
|
||||
skin_name = String()
|
||||
|
||||
class CustomisedSkin(RowModel):
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
CREATE TABLE customised_skins(
|
||||
custom_skin_id SERIAL PRIMARY KEY,
|
||||
base_skin_id INTEGER REFERENCES global_available_skins (skin_id),
|
||||
_timestamp TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
"""
|
||||
_tablename_ = 'customised_skins'
|
||||
|
||||
custom_skin_id = Integer(primary=True)
|
||||
base_skin_id = Integer()
|
||||
|
||||
_timestamp = Timestamp()
|
||||
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
CREATE TABLE customised_skin_property_ids(
|
||||
property_id SERIAL PRIMARY KEY,
|
||||
card_id TEXT NOT NULL,
|
||||
property_name TEXT NOT NULL,
|
||||
UNIQUE(card_id, property_name)
|
||||
);
|
||||
"""
|
||||
skin_property_map = Table('customised_skin_property_ids')
|
||||
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
CREATE TABLE customised_skin_properties(
|
||||
custom_skin_id INTEGER NOT NULL REFERENCES customised_skins (custom_skin_id),
|
||||
property_id INTEGER NOT NULL REFERENCES customised_skin_property_ids (property_id),
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY (custom_skin_id, property_id)
|
||||
);
|
||||
CREATE INDEX customised_skin_property_skin_id ON customised_skin_properties(custom_skin_id);
|
||||
"""
|
||||
skin_properties = Table('customised_skin_properties')
|
||||
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
CREATE VIEW customised_skin_data AS
|
||||
SELECT
|
||||
skins.custom_skin_id AS custom_skin_id,
|
||||
skins.base_skin_id AS base_skin_id,
|
||||
properties.property_id AS property_id,
|
||||
prop_ids.card_id AS card_id,
|
||||
prop_ids.property_name AS property_name,
|
||||
properties.value AS value
|
||||
FROM
|
||||
customised_skins skins
|
||||
LEFT JOIN customised_skin_properties properties ON skins.custom_skin_id = properties.custom_skin_id
|
||||
LEFT JOIN customised_skin_property_ids prop_ids ON properties.property_id = prop_ids.property_id;
|
||||
"""
|
||||
custom_skin_info = Table('customised_skin_data')
|
||||
|
||||
class UserSkin(RowModel):
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
CREATE TABLE user_skin_inventory(
|
||||
itemid SERIAL PRIMARY KEY,
|
||||
userid BIGINT NOT NULL REFERENCES user_config (userid) ON DELETE CASCADE,
|
||||
custom_skin_id INTEGER NOT NULL REFERENCES customised_skins (custom_skin_id) ON DELETE CASCADE,
|
||||
transactionid INTEGER REFERENCES gem_transactions (transactionid),
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
acquired_at TIMESTAMPTZ DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX user_skin_inventory_users ON user_skin_inventory(userid);
|
||||
CREATE UNIQUE INDEX user_skin_inventory_active ON user_skin_inventory(userid) WHERE active = TRUE;
|
||||
"""
|
||||
_tablename_ = 'user_skin_inventory'
|
||||
|
||||
itemid = Integer(primary=True)
|
||||
userid = Integer()
|
||||
custom_skin_id = Integer()
|
||||
transactionid = Integer()
|
||||
active = Bool()
|
||||
acquired_at = Timestamp()
|
||||
expires_at = Timestamp()
|
||||
|
||||
"""
|
||||
Schema
|
||||
------
|
||||
CREATE VIEW user_active_skins AS
|
||||
SELECT
|
||||
*
|
||||
FROM user_skin_inventory
|
||||
WHERE active=True;
|
||||
"""
|
||||
user_active_skins = Table('user_active_skins')
|
||||
0
src/modules/skins/editor/__init__.py
Normal file
0
src/modules/skins/editor/__init__.py
Normal file
133
src/modules/skins/editor/layout.py
Normal file
133
src/modules/skins/editor/layout.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import uuid
|
||||
import discord
|
||||
from discord.components import SelectOption
|
||||
|
||||
from babel.translator import LazyStr
|
||||
from gui.base.Card import Card
|
||||
from utils.lib import EmbedField, tabulate
|
||||
|
||||
from .skinsetting import SettingInputType, Setting
|
||||
from ..skinlib import CustomSkin
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingGroup:
|
||||
"""
|
||||
Data class representing a collection of settings which are naturally
|
||||
grouped together at interface level.
|
||||
|
||||
Typically the settings in a single SettingGroup are displayed
|
||||
in the same embed field, the settings are edited with the same modal,
|
||||
and the group represents a single option in the "setting group menu".
|
||||
|
||||
Setting groups do not correspond to any grouping at the Card or Skin level,
|
||||
and may cross multiple cards.
|
||||
"""
|
||||
|
||||
# The name and description strings are shown in the embed field and menu option
|
||||
name: LazyStr
|
||||
|
||||
# Tuple of settings that are part of this setting group
|
||||
settings: tuple[Setting, ...]
|
||||
|
||||
description: Optional[LazyStr] = None
|
||||
|
||||
# Whether the group should be displayed in a group or not
|
||||
ungrouped: bool = False
|
||||
|
||||
# Whether the embed field should be inline
|
||||
inline: bool = True
|
||||
|
||||
# Component custom id to identify the editing component
|
||||
# Also used as the value of the select option
|
||||
custom_id: str = str(uuid.uuid4())
|
||||
|
||||
@property
|
||||
def editable_settings(self):
|
||||
return tuple(setting for setting in self.settings if setting.input_type is SettingInputType.ModalInput)
|
||||
|
||||
def embed_field_for(self, skin: CustomSkin) -> EmbedField:
|
||||
"""
|
||||
Tabulates the contained settings and builds an embed field for the editor UI.
|
||||
"""
|
||||
t = skin.bot.translator.t
|
||||
|
||||
rows: list[tuple[str, str]] = []
|
||||
for setting in self.settings:
|
||||
name = t(setting.display_name)
|
||||
value = setting.value_in(skin) or setting.default_value_in(skin)
|
||||
formatted = setting.format_value_in(skin, value)
|
||||
rows.append((name, formatted))
|
||||
|
||||
lines = tabulate(*rows)
|
||||
table = '\n'.join(lines)
|
||||
|
||||
description = f"*{t(self.description)}*" if self.description else ''
|
||||
|
||||
embed_field = EmbedField(
|
||||
name=t(self.name),
|
||||
value=f"{description}\n{table}",
|
||||
inline=self.inline,
|
||||
)
|
||||
return embed_field
|
||||
|
||||
def select_option_for(self, skin: CustomSkin) -> SelectOption:
|
||||
"""
|
||||
Makes a SelectOption referring to this setting group.
|
||||
"""
|
||||
t = skin.bot.translator.t
|
||||
option = SelectOption(
|
||||
label=t(self.name),
|
||||
description=t(self.description) if self.description else None,
|
||||
value=self.custom_id,
|
||||
)
|
||||
return option
|
||||
|
||||
|
||||
@dataclass
|
||||
class Page:
|
||||
"""
|
||||
Represents a page of skin settings for the skin editor UI.
|
||||
"""
|
||||
# Various string attributes of the page
|
||||
display_name: LazyStr
|
||||
editing_description: Optional[LazyStr] = None
|
||||
preview_description: Optional[LazyStr] = None
|
||||
|
||||
visible_in_preview: bool = True
|
||||
render_card: Optional[type[Card]] = None
|
||||
|
||||
groups: list[SettingGroup] = field(default_factory=list)
|
||||
|
||||
def make_embed_for(self, skin: CustomSkin) -> discord.Embed:
|
||||
t = skin.bot.translator.t
|
||||
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.orange(),
|
||||
title=t(self.display_name),
|
||||
)
|
||||
|
||||
description_lines: list[str] = []
|
||||
field_counter = 0
|
||||
|
||||
for group in self.groups:
|
||||
field = group.embed_field_for(skin)
|
||||
if group.ungrouped:
|
||||
description_lines.append(field.value)
|
||||
else:
|
||||
embed.add_field(**field._asdict())
|
||||
if not (field_counter) % 3:
|
||||
embed.add_field(name='', value='')
|
||||
field_counter += 1
|
||||
field_counter += 1
|
||||
|
||||
if description_lines:
|
||||
embed.description = '\n'.join(description_lines)
|
||||
|
||||
if self.render_card is not None:
|
||||
embed.set_image(url='attachment://sample.png')
|
||||
|
||||
return embed
|
||||
16
src/modules/skins/editor/pages/__init__.py
Normal file
16
src/modules/skins/editor/pages/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from .stats import stats_page
|
||||
from .profile import profile_page
|
||||
from .summary import summary_page
|
||||
from .weekly import weekly_page
|
||||
from .monthly import monthly_page
|
||||
from .weekly_goals import weekly_goal_page
|
||||
from .monthly_goals import monthly_goal_page
|
||||
from .leaderboard import leaderboard_page
|
||||
|
||||
|
||||
pages = [
|
||||
profile_page, stats_page,
|
||||
weekly_page, monthly_page,
|
||||
weekly_goal_page, monthly_goal_page,
|
||||
leaderboard_page,
|
||||
]
|
||||
294
src/modules/skins/editor/pages/leaderboard.py
Normal file
294
src/modules/skins/editor/pages/leaderboard.py
Normal file
@@ -0,0 +1,294 @@
|
||||
from gui.cards import LeaderboardCard
|
||||
|
||||
from ... import babel
|
||||
from ..skinsetting import ColourSetting, SkinSetting, ColoursSetting
|
||||
from ..layout import Page, SettingGroup
|
||||
|
||||
_p = babel._p
|
||||
|
||||
"""
|
||||
top_position_colour
|
||||
top_name_colour
|
||||
top_hours_colour
|
||||
|
||||
entry_position_colour
|
||||
entry_position_highlight_colour
|
||||
entry_name_colour
|
||||
entry_hours_colour
|
||||
|
||||
header_text_colour
|
||||
[subheader_name_colour, subheader_value_colour]
|
||||
|
||||
entry_bg_colour
|
||||
entry_bg_highlight_colour
|
||||
"""
|
||||
|
||||
leaderboard_page = Page(
|
||||
display_name=_p('skinsettings|page:leaderboard|display_name', "Leaderboard"),
|
||||
editing_description=_p(
|
||||
'skinsettings|page:leaderboard|edit_desc',
|
||||
"Options for the Leaderboard pages."
|
||||
),
|
||||
preview_description=None,
|
||||
visible_in_preview=True,
|
||||
render_card=LeaderboardCard
|
||||
)
|
||||
|
||||
header_text_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='header_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:header_text_colour|display_name',
|
||||
"Header"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:header_text_colour|desc',
|
||||
"Text colour of the leaderboard header."
|
||||
)
|
||||
)
|
||||
|
||||
subheader_name_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='subheader_name_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:subheader_name_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:subheader_name_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
subheader_value_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='subheader_value_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:subheader_value_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:subheader_value_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
subheader_colour = ColoursSetting(
|
||||
subheader_value_colour,
|
||||
subheader_name_colour,
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:subheader_colour|display_name',
|
||||
"Sub-Header"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:subheader_colour|desc',
|
||||
"Text colour of the sub-header line."
|
||||
)
|
||||
)
|
||||
|
||||
top_position_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='top_position_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:top_position_colour|display_name',
|
||||
"Position"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:top_position_colour|desc',
|
||||
"Top 3 position colour."
|
||||
)
|
||||
)
|
||||
|
||||
top_name_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='top_name_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:top_name_colour|display_name',
|
||||
"Name"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:top_name_colour|desc',
|
||||
"Top 3 name colour."
|
||||
)
|
||||
)
|
||||
|
||||
top_hours_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='top_hours_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:top_hours_colour|display_name',
|
||||
"Hours"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:top_hours_colour|desc',
|
||||
"Top 3 hours colour."
|
||||
)
|
||||
)
|
||||
|
||||
entry_position_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='entry_position_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_position_colour|display_name',
|
||||
"Position"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_position_colour|desc',
|
||||
"Position text colour."
|
||||
)
|
||||
)
|
||||
|
||||
entry_position_highlight_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='entry_position_highlight_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_position_highlight_colour|display_name',
|
||||
"Position (HL)"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_position_highlight_colour|desc',
|
||||
"Highlighted position colour."
|
||||
)
|
||||
)
|
||||
|
||||
entry_name_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='entry_name_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_name_colour|display_name',
|
||||
"Name"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_name_colour|desc',
|
||||
"Entry name colour."
|
||||
)
|
||||
)
|
||||
|
||||
entry_hours_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='entry_hours_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_hours_colour|display_name',
|
||||
"Hours"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_hours_colour|desc',
|
||||
"Entry hours colour."
|
||||
)
|
||||
)
|
||||
|
||||
entry_bg_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='entry_bg_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_bg_colour|display_name',
|
||||
"Regular"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_bg_colour|desc',
|
||||
"Background colour of regular entries."
|
||||
)
|
||||
)
|
||||
|
||||
entry_bg_highlight_colour = ColourSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='entry_bg_highlight_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_bg_highlight_colour|display_name',
|
||||
"Highlighted"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:entry_bg_highlight_colour|desc',
|
||||
"Background colour of highlighted entries."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
top_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:leaderboard|grp:top_colour', "Top 3"),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|grp:top_colour|desc',
|
||||
"Customise the text colours for the top 3 positions."
|
||||
),
|
||||
custom_id='leaderboard-top',
|
||||
settings=(
|
||||
top_position_colour,
|
||||
top_name_colour,
|
||||
top_hours_colour
|
||||
)
|
||||
)
|
||||
|
||||
entry_text_group = SettingGroup(
|
||||
_p('skinsettings|page:leaderboard|grp:entry_text', "Entry Text"),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|grp:entry_text|desc',
|
||||
"Text colours of the leaderboard entries."
|
||||
),
|
||||
custom_id='leaderboard-text',
|
||||
settings=(
|
||||
entry_position_colour,
|
||||
entry_position_highlight_colour,
|
||||
entry_name_colour,
|
||||
entry_hours_colour
|
||||
)
|
||||
)
|
||||
|
||||
entry_bg_group = SettingGroup(
|
||||
_p('skinsettings|page:leaderboard|grp:entry_bg', "Entry Background"),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|grp:entry_bg|desc',
|
||||
"Background colours of the leaderboard entries."
|
||||
),
|
||||
custom_id='leaderboard-bg',
|
||||
settings=(
|
||||
entry_bg_colour,
|
||||
entry_bg_highlight_colour
|
||||
)
|
||||
)
|
||||
|
||||
misc_group = SettingGroup(
|
||||
_p('skinsettings|page:leaderboard|grp:misc', "Miscellaneous"),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|grp:misc|desc',
|
||||
"Other miscellaneous colour settings."
|
||||
),
|
||||
custom_id='leaderboard-misc',
|
||||
settings=(
|
||||
header_text_colour,
|
||||
subheader_colour
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
base_skin = SkinSetting(
|
||||
card=LeaderboardCard,
|
||||
property_name='base_skin_id',
|
||||
display_name=_p(
|
||||
'skinsettings|page:leaderboard|set:base_skin|display_name',
|
||||
'Skin'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|set:base_skin|desc',
|
||||
"Select a Skin Preset."
|
||||
)
|
||||
)
|
||||
|
||||
base_skin_group = SettingGroup(
|
||||
_p('skinsettings|page:leaderboard|grp:base_skin', "Leaderboard Skin"),
|
||||
description=_p(
|
||||
'skinsettings|page:leaderboard|grp:base_skin|desc',
|
||||
"Asset pack and default values for the Leaderboard."
|
||||
),
|
||||
custom_id='leaderboard-skin',
|
||||
settings=(base_skin,),
|
||||
ungrouped=True
|
||||
)
|
||||
|
||||
leaderboard_page.groups = [
|
||||
base_skin_group,
|
||||
top_colour_group,
|
||||
entry_text_group,
|
||||
entry_bg_group,
|
||||
misc_group
|
||||
]
|
||||
|
||||
376
src/modules/skins/editor/pages/monthly.py
Normal file
376
src/modules/skins/editor/pages/monthly.py
Normal file
@@ -0,0 +1,376 @@
|
||||
from gui.cards import MonthlyStatsCard
|
||||
|
||||
from ... import babel
|
||||
from ..skinsetting import ColourSetting, SkinSetting, ColoursSetting
|
||||
from ..layout import Page, SettingGroup
|
||||
|
||||
_p = babel._p
|
||||
|
||||
""""
|
||||
title_colour
|
||||
[this_month_colour, last_month_colour]
|
||||
[stats_key_colour, stats_value_colour]
|
||||
footer_colour
|
||||
|
||||
top_hours_colour
|
||||
top_hours_bg_colour
|
||||
top_date_colour
|
||||
top_line_colour
|
||||
|
||||
top_this_colour
|
||||
top_this_hours_colour
|
||||
top_last_colour
|
||||
top_last_hours_colour
|
||||
|
||||
weekday_background_colour
|
||||
weekday_colour
|
||||
month_background_colour
|
||||
month_colour
|
||||
"""
|
||||
|
||||
monthly_page = Page(
|
||||
display_name=_p('skinsettings|page:monthly|display_name', "Monthly Statistics"),
|
||||
editing_description=_p(
|
||||
'skinsettings|page:monthly|edit_desc',
|
||||
"Options for the monthly statistis card."
|
||||
),
|
||||
preview_description=None,
|
||||
visible_in_preview=True,
|
||||
render_card=MonthlyStatsCard
|
||||
)
|
||||
|
||||
|
||||
title_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='title_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:title_colour|display_name',
|
||||
'Title'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:title_colour|desc',
|
||||
"Colour of the card title."
|
||||
)
|
||||
)
|
||||
top_hours_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='top_hours_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:top_hours_colour|display_name',
|
||||
'Hours'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:top_hours_colour|desc',
|
||||
"Hour axis labels."
|
||||
)
|
||||
)
|
||||
top_hours_bg_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='top_hours_bg_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:top_hours_bg_colour|display_name',
|
||||
'Hours Bg'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:top_hours_bg_colour|desc',
|
||||
"Hour axis label background."
|
||||
)
|
||||
)
|
||||
top_line_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='top_line_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:top_line_colour|display_name',
|
||||
'Lines'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:top_line_colour|desc',
|
||||
"Horizontal graph lines."
|
||||
)
|
||||
)
|
||||
top_date_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='top_date_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:top_date_colour|display_name',
|
||||
'Days'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:top_date_colour|desc',
|
||||
"Day axis labels."
|
||||
)
|
||||
)
|
||||
top_this_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='top_this_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:top_this_colour|display_name',
|
||||
'This Month Bar'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:top_this_colour|desc',
|
||||
"This month bar fill colour."
|
||||
)
|
||||
)
|
||||
top_last_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='top_last_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:top_last_colour|display_name',
|
||||
'Last Month Bar'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:top_last_colour|desc',
|
||||
"Last month bar fill colour."
|
||||
)
|
||||
)
|
||||
top_this_hours_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='top_this_hours_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:top_this_hours_colour|display_name',
|
||||
'This Month Hours'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:top_this_hours_colour|desc',
|
||||
"This month hour text."
|
||||
)
|
||||
)
|
||||
top_last_hours_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='top_last_hours_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:top_last_hours_colour|display_name',
|
||||
'Last Month Hours'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:top_last_hours_colour|desc',
|
||||
"Last month hour text."
|
||||
)
|
||||
)
|
||||
this_month_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='this_month_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:this_month_colour|display_name',
|
||||
'This Month Legend'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:this_month_colour|desc',
|
||||
"This month legend text."
|
||||
)
|
||||
)
|
||||
last_month_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='last_month_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:last_month_colour|display_name',
|
||||
'Last Month Legend'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:last_month_colour|desc',
|
||||
"Last month legend text."
|
||||
)
|
||||
)
|
||||
legend_colour = ColoursSetting(
|
||||
this_month_colour,
|
||||
last_month_colour,
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:legend_colour|display_name',
|
||||
'Legend'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:legend_colour|desc',
|
||||
"Legend text colour."
|
||||
)
|
||||
)
|
||||
|
||||
weekday_background_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='weekday_background_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:weekday_background_colour|display_name',
|
||||
'Weekday Bg'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:weekday_background_colour|desc',
|
||||
"Weekday axis background colour."
|
||||
)
|
||||
)
|
||||
weekday_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='weekday_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:weekday_colour|display_name',
|
||||
'Weekdays'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:weekday_colour|desc',
|
||||
"Weekday axis labels."
|
||||
)
|
||||
)
|
||||
month_background_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='month_background_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:month_background_colour|display_name',
|
||||
'Month Bg'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:month_background_colour|desc',
|
||||
"Month axis label backgrounds."
|
||||
)
|
||||
)
|
||||
month_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='month_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:month_colour|display_name',
|
||||
'Months'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:month_colour|desc',
|
||||
"Month axis label text."
|
||||
)
|
||||
)
|
||||
stats_key_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='stats_key_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:stats_key_colour|display_name',
|
||||
'Stat Names'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:stats_key_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
stats_value_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='stats_value_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:stats_value_colour|display_name',
|
||||
'Stat Values'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:stats_value_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
statistics_colour = ColoursSetting(
|
||||
stats_key_colour,
|
||||
stats_value_colour,
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:statistics_colour|display_name',
|
||||
'Statistics'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:statistics_colour|desc',
|
||||
"Summary Statistics"
|
||||
)
|
||||
)
|
||||
footer_colour = ColourSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='footer_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:footer_colour|display_name',
|
||||
'Footer'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:footer_colour|desc',
|
||||
"Footer text colour."
|
||||
)
|
||||
)
|
||||
|
||||
top_graph_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly|grp:top_graph', "Top Graph"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|grp:top_graph|desc',
|
||||
"Customise the axis and style of the top graph."
|
||||
),
|
||||
custom_id='monthly-top',
|
||||
settings=(
|
||||
top_hours_colour,
|
||||
top_hours_bg_colour,
|
||||
top_date_colour,
|
||||
top_line_colour
|
||||
)
|
||||
)
|
||||
|
||||
top_hours_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly|grp:top_hours', "Hours"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|grp:top_hours|desc',
|
||||
"Customise the colour of this week and last week."
|
||||
),
|
||||
custom_id='monthly-hours',
|
||||
settings=(
|
||||
top_this_colour,
|
||||
top_this_hours_colour,
|
||||
top_last_colour,
|
||||
top_last_hours_colour
|
||||
)
|
||||
)
|
||||
|
||||
bottom_graph_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly|grp:bottom_graph', "Heatmap"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|grp:bottom_graph|desc',
|
||||
"Customise the axis and style of the heatmap."
|
||||
),
|
||||
custom_id='monthly-heatmap',
|
||||
settings=(
|
||||
weekday_background_colour,
|
||||
weekday_colour,
|
||||
month_background_colour,
|
||||
month_colour
|
||||
)
|
||||
)
|
||||
|
||||
misc_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly|grp:misc', "Miscellaneous"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|grp:misc|desc',
|
||||
"Miscellaneous colour options."
|
||||
),
|
||||
custom_id='monthly-misc',
|
||||
settings=(
|
||||
title_colour,
|
||||
legend_colour,
|
||||
statistics_colour,
|
||||
footer_colour
|
||||
)
|
||||
)
|
||||
|
||||
base_skin = SkinSetting(
|
||||
card=MonthlyStatsCard,
|
||||
property_name='base_skin_id',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly|set:base_skin|display_name',
|
||||
'Skin'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|set:base_skin|desc',
|
||||
"Select a Skin Preset."
|
||||
)
|
||||
)
|
||||
|
||||
base_skin_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly|grp:base_skin', "Monthly Stats Skin"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly|grp:base_skin|desc',
|
||||
"Asset pack and default values for the Monthly Statistics."
|
||||
),
|
||||
custom_id='monthly-skin',
|
||||
settings=(base_skin,),
|
||||
ungrouped=True
|
||||
)
|
||||
|
||||
monthly_page.groups = [
|
||||
base_skin_group,
|
||||
top_graph_group,
|
||||
top_hours_group,
|
||||
bottom_graph_group,
|
||||
misc_group
|
||||
]
|
||||
|
||||
436
src/modules/skins/editor/pages/monthly_goals.py
Normal file
436
src/modules/skins/editor/pages/monthly_goals.py
Normal file
@@ -0,0 +1,436 @@
|
||||
from gui.cards import MonthlyGoalCard
|
||||
|
||||
from ... import babel
|
||||
from ..skinsetting import ColourSetting, SkinSetting, ColoursSetting
|
||||
from ..layout import Page, SettingGroup
|
||||
|
||||
_p = babel._p
|
||||
|
||||
"""
|
||||
mini_profile_name_colour
|
||||
mini_profile_badge_colour
|
||||
mini_profile_badge_text_colour
|
||||
mini_profile_discrim_colour
|
||||
|
||||
title_colour
|
||||
footer_colour
|
||||
|
||||
progress_bg_colour
|
||||
progress_colour
|
||||
[attendance_rate_colour, task_count_colour, studied_hour_colour]
|
||||
[attendance_colour, task_done_colour, studied_text_colour, task_goal_colour]
|
||||
task_goal_number_colour
|
||||
|
||||
task_header_colour
|
||||
task_done_number_colour
|
||||
task_undone_number_colour
|
||||
task_done_text_colour
|
||||
task_undone_text_colour
|
||||
"""
|
||||
|
||||
monthly_goal_page = Page(
|
||||
display_name=_p('skinsettings|page:monthly_goal|display_name', "Monthly Goals"),
|
||||
editing_description=_p(
|
||||
'skinsettings|page:monthly_goal|edit_desc',
|
||||
"Options for the monthly goal card."
|
||||
),
|
||||
preview_description=None,
|
||||
visible_in_preview=True,
|
||||
render_card=MonthlyGoalCard
|
||||
)
|
||||
|
||||
title_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='title_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:title_colour|display_name',
|
||||
"Title"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:title_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
progress_bg_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='progress_bg_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:progress_bg_colour|display_name',
|
||||
"Bar Bg"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:progress_bg_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
progress_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='progress_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:progress_colour|display_name',
|
||||
"Bar Fg"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:progress_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
attendance_rate_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='attendance_rate_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:attendance_rate_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:attendance_rate_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
attendance_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='attendance_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:attendance_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:attendance_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_count_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='task_count_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_count_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_count_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_done_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='task_done_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_done_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_done_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_goal_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='task_goal_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_goal_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_goal_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_goal_number_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='task_goal_number_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_goal_number_colour|display_name',
|
||||
"Task Goal"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_goal_number_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
studied_text_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='studied_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:studied_text_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:studied_text_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
studied_hour_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='studied_hour_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:studied_hour_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:studied_hour_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
text_highlight_colour = ColoursSetting(
|
||||
attendance_rate_colour,
|
||||
task_count_colour,
|
||||
studied_hour_colour,
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:text_highlight_colour|display_name',
|
||||
"Highlight Text"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:text_highlight_colour|desc',
|
||||
"Progress text colour."
|
||||
)
|
||||
)
|
||||
|
||||
text_colour = ColoursSetting(
|
||||
attendance_colour,
|
||||
task_done_colour,
|
||||
studied_text_colour,
|
||||
task_goal_colour,
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:text_colour|display_name',
|
||||
"Text"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:text_colour|desc',
|
||||
"Achievement description text colour."
|
||||
)
|
||||
)
|
||||
|
||||
task_header_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='task_header_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_header_colour|display_name',
|
||||
"Task Header"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_header_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_done_number_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='task_done_number_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_done_number_colour|display_name',
|
||||
"Checked Number"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_done_number_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_undone_number_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='task_undone_number_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_undone_number_colour|display_name',
|
||||
"Unchecked Number"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_undone_number_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_done_text_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='task_done_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_done_text_colour|display_name',
|
||||
"Checked Text"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_done_text_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_undone_text_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='task_undone_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_undone_text_colour|display_name',
|
||||
"Unchecked Text"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:task_undone_text_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
footer_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='footer_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:footer_colour|display_name',
|
||||
"Footer"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:footer_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
mini_profile_badge_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='mini_profile_badge_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:mini_profile_badge_colour|display_name',
|
||||
'Badge Background'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:mini_profile_badge_colour|desc',
|
||||
"Mini-profile badge background colour."
|
||||
)
|
||||
)
|
||||
|
||||
mini_profile_badge_text_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='mini_profile_badge_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:mini_profile_badge_text_colour|display_name',
|
||||
'Badge Text'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:mini_profile_badge_text_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
mini_profile_name_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='mini_profile_name_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:mini_profile_name_colour|display_name',
|
||||
'Username'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:mini_profile_name_colour|desc',
|
||||
"Mini-profile username colour."
|
||||
)
|
||||
)
|
||||
|
||||
mini_profile_discrim_colour = ColourSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='mini_profile_discrim_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:mini_profile_discrim_colour|display_name',
|
||||
'Discriminator'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:mini_profile_discrim_colour|desc',
|
||||
"Mini-profile discriminator colour."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
mini_profile_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly_goal|grp:mini_profile', "Profile"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|grp:mini_profile|desc',
|
||||
"Customise the mini-profile shown above the goals."
|
||||
),
|
||||
custom_id='monthlygoal-profile',
|
||||
settings=(
|
||||
mini_profile_name_colour,
|
||||
mini_profile_discrim_colour,
|
||||
mini_profile_badge_colour,
|
||||
mini_profile_badge_text_colour,
|
||||
)
|
||||
)
|
||||
|
||||
misc_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly_goal|grp:misc', "Miscellaneous"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|grp:misc|desc',
|
||||
"Other miscellaneous colours."
|
||||
),
|
||||
custom_id='monthlygoal-misc',
|
||||
settings=(
|
||||
title_colour,
|
||||
footer_colour,
|
||||
)
|
||||
)
|
||||
|
||||
task_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly_goal|grp:task_colour', "Task colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|grp:task_colour|desc',
|
||||
"Text and number colours for (in)complete goals."
|
||||
),
|
||||
custom_id='monthlygoal-tasks',
|
||||
settings=(
|
||||
task_undone_number_colour,
|
||||
task_done_number_colour,
|
||||
task_undone_text_colour,
|
||||
task_done_text_colour,
|
||||
)
|
||||
)
|
||||
|
||||
progress_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly_goal|grp:progress_colour', "Progress Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|grp:progress_colour|desc',
|
||||
"Customise colours for the monthly achievement progress."
|
||||
),
|
||||
custom_id='monthlygoal-progress',
|
||||
settings=(
|
||||
progress_bg_colour,
|
||||
progress_colour,
|
||||
text_colour,
|
||||
text_highlight_colour,
|
||||
task_goal_number_colour
|
||||
)
|
||||
)
|
||||
|
||||
base_skin = SkinSetting(
|
||||
card=MonthlyGoalCard,
|
||||
property_name='base_skin_id',
|
||||
display_name=_p(
|
||||
'skinsettings|page:monthly_goal|set:base_skin|display_name',
|
||||
'Skin'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|set:base_skin|desc',
|
||||
"Select a Skin Preset."
|
||||
)
|
||||
)
|
||||
|
||||
base_skin_group = SettingGroup(
|
||||
_p('skinsettings|page:monthly_goal|grp:base_skin', "Monthly Goals Skin"),
|
||||
description=_p(
|
||||
'skinsettings|page:monthly_goal|grp:base_skin|desc',
|
||||
"Asset pack and default values for the Monthly Goals."
|
||||
),
|
||||
custom_id='monthlygoals-skin',
|
||||
settings=(base_skin,),
|
||||
ungrouped=True
|
||||
)
|
||||
|
||||
monthly_goal_page.groups = [
|
||||
base_skin_group,
|
||||
mini_profile_group,
|
||||
misc_group,
|
||||
progress_colour_group,
|
||||
task_colour_group
|
||||
]
|
||||
|
||||
266
src/modules/skins/editor/pages/profile.py
Normal file
266
src/modules/skins/editor/pages/profile.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from gui.cards import ProfileCard
|
||||
|
||||
from ... import babel
|
||||
from ..skinsetting import ColourSetting, SkinSetting, ColoursSetting
|
||||
from ..layout import Page, SettingGroup
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
|
||||
profile_page = Page(
|
||||
display_name=_p('skinsettings|page:profile|display_name', "Member Profile"),
|
||||
editing_description=_p(
|
||||
'skinsettings|page:profile|edit_desc',
|
||||
"Options for the member profile card."
|
||||
),
|
||||
preview_description=None,
|
||||
visible_in_preview=True,
|
||||
render_card=ProfileCard
|
||||
)
|
||||
|
||||
|
||||
header_colour_1 = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='header_colour_1',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:header_colour_1|display_name',
|
||||
'Username'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:header_colour_1|desc',
|
||||
"Text colour of the profile username."
|
||||
)
|
||||
)
|
||||
|
||||
header_colour_2 = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='header_colour_2',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:header_colour_2|display_name',
|
||||
'Discriminator'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:header_colour_2|desc',
|
||||
"Text colour of the profile dscriminator."
|
||||
)
|
||||
)
|
||||
|
||||
counter_bg_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='counter_bg_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:counter_bg_colour|display_name',
|
||||
'Background'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:counter_bg_colour|desc',
|
||||
"Colour of the coin/gem/gift backgrounds."
|
||||
)
|
||||
)
|
||||
|
||||
counter_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='counter_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:counter_colour|display_name',
|
||||
'Text'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:counter_colour|desc',
|
||||
"Colour of the coin/gem/gift text."
|
||||
)
|
||||
)
|
||||
|
||||
subheader_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='subheader_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:subheader_colour|display_name',
|
||||
'Column Header'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:subheader_colour|desc',
|
||||
"Colour of the Profile/Achievements header."
|
||||
)
|
||||
)
|
||||
|
||||
badge_text_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='badge_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:badge_text_colour|display_name',
|
||||
'Badge Text'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:badge_text_colour|desc',
|
||||
"Colour of the profile badge text."
|
||||
)
|
||||
)
|
||||
|
||||
badge_blob_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='badge_blob_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:badge_blob_colour|display_name',
|
||||
'Background'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:badge_blob_colour|desc',
|
||||
"Colour of the profile badge background."
|
||||
)
|
||||
)
|
||||
|
||||
rank_name_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='rank_name_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:rank_name_colour|display_name',
|
||||
'Current Rank'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:rank_name_colour|desc',
|
||||
"Colour of the current study rank name."
|
||||
)
|
||||
)
|
||||
|
||||
rank_hours_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='rank_hours_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:rank_hours_colour|display_name',
|
||||
'Required Hours'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:rank_hours_colour|desc',
|
||||
"Colour of the study rank hour range."
|
||||
)
|
||||
)
|
||||
|
||||
bar_full_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='bar_full_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:bar_full_colour|display_name',
|
||||
'Bar Full'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:bar_full_colour|desc',
|
||||
"Foreground progress bar colour."
|
||||
)
|
||||
)
|
||||
|
||||
bar_empty_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='bar_empty_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:bar_empty_colour|display_name',
|
||||
'Bar Empty'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:bar_empty_colour|desc',
|
||||
"Background progress bar colour."
|
||||
)
|
||||
)
|
||||
|
||||
next_rank_colour = ColourSetting(
|
||||
card=ProfileCard,
|
||||
property_name='next_rank_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:next_rank_colour|display_name',
|
||||
'Next Rank'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:next_rank_colour|desc',
|
||||
"Colour of the next rank name and hours."
|
||||
)
|
||||
)
|
||||
|
||||
title_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:profile|grp:title_colour', "Title Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|grp:title_colour|desc',
|
||||
"Header and suheader text colours."
|
||||
),
|
||||
custom_id='profile-titles',
|
||||
settings=(
|
||||
header_colour_1,
|
||||
header_colour_2,
|
||||
subheader_colour
|
||||
),
|
||||
)
|
||||
|
||||
badge_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:profile|grp:badge_colour', "Profile Badge Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|grp:badge_colour|desc',
|
||||
"Text and background for the profile badges."
|
||||
),
|
||||
custom_id='profile-badges',
|
||||
settings=(
|
||||
badge_text_colour,
|
||||
badge_blob_colour
|
||||
),
|
||||
)
|
||||
|
||||
counter_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:profile|grp:counter_colour', "Counter Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|grp:counter_colour|desc',
|
||||
"Text and background for the coin/gem/gift counters."
|
||||
),
|
||||
custom_id='profile-counters',
|
||||
settings=(
|
||||
counter_colour,
|
||||
counter_bg_colour
|
||||
),
|
||||
)
|
||||
|
||||
rank_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:profile|grp:rank_colour', "Progress Bar"),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|grp:rank_colour|desc',
|
||||
"Colours for the study badge/rank progress bar."
|
||||
),
|
||||
custom_id='profile-progress',
|
||||
settings=(
|
||||
rank_name_colour,
|
||||
rank_hours_colour,
|
||||
next_rank_colour,
|
||||
bar_full_colour,
|
||||
bar_empty_colour
|
||||
),
|
||||
)
|
||||
|
||||
base_skin = SkinSetting(
|
||||
card=ProfileCard,
|
||||
property_name='base_skin_id',
|
||||
display_name=_p(
|
||||
'skinsettings|page:profile|set:base_skin|display_name',
|
||||
'Skin'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|set:base_skin|desc',
|
||||
"Select a Skin Preset."
|
||||
)
|
||||
)
|
||||
|
||||
base_skin_group = SettingGroup(
|
||||
_p('skinsettings|page:profile|grp:base_skin', "Profile Skin"),
|
||||
description=_p(
|
||||
'skinsettings|page:profile|grp:base_skin|desc',
|
||||
"Asset pack and default values for this card."
|
||||
),
|
||||
custom_id='profile-skin',
|
||||
settings=(base_skin,),
|
||||
ungrouped=True
|
||||
)
|
||||
|
||||
profile_page.groups = [
|
||||
base_skin_group,
|
||||
title_colour_group,
|
||||
badge_colour_group,
|
||||
rank_colour_group,
|
||||
counter_colour_group,
|
||||
]
|
||||
|
||||
211
src/modules/skins/editor/pages/stats.py
Normal file
211
src/modules/skins/editor/pages/stats.py
Normal file
@@ -0,0 +1,211 @@
|
||||
from gui.cards import StatsCard
|
||||
|
||||
from ... import babel
|
||||
from ..skinsetting import ColourSetting, SkinSetting, ColoursSetting
|
||||
from ..layout import Page, SettingGroup
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
stats_page = Page(
|
||||
display_name=_p('skinsettings|page:stats|display_name', "Statistics"),
|
||||
editing_description=_p(
|
||||
'skinsettings|page:stats|edit_desc',
|
||||
"Options for the member statistics card."
|
||||
),
|
||||
preview_description=None,
|
||||
visible_in_preview=True,
|
||||
render_card=StatsCard
|
||||
)
|
||||
|
||||
|
||||
header_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='header_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:header_colour|display_name',
|
||||
'Titles'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:header_colour|desc',
|
||||
"Top header text colour."
|
||||
)
|
||||
)
|
||||
|
||||
stats_subheader_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='stats_subheader_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:stats_subheader_colour|display_name',
|
||||
'Sections'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:stats_subheader_colour|desc',
|
||||
"Text colour of the Statistics section titles."
|
||||
)
|
||||
)
|
||||
|
||||
stats_text_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='stats_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:stats_text_colour|display_name',
|
||||
'Statistics'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:stats_text_colour|desc',
|
||||
"Text colour of the Statistics section bodies."
|
||||
)
|
||||
)
|
||||
|
||||
col2_date_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='col2_date_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:col2_date_colour|display_name',
|
||||
'Date'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:col2_date_colour|desc',
|
||||
"Colour of the current month and year."
|
||||
)
|
||||
)
|
||||
|
||||
col2_hours_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='col2_hours_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:col2_hours_colour|display_name',
|
||||
'Hours'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:col2_hours_colour|desc',
|
||||
"Colour of the monthly hour total."
|
||||
)
|
||||
)
|
||||
|
||||
text_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:stats|grp:text_colour', "Text Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|grp:text_colour|desc',
|
||||
"Header colours and statistics text colours."
|
||||
),
|
||||
custom_id='stats-text',
|
||||
settings=(
|
||||
header_colour,
|
||||
stats_subheader_colour,
|
||||
stats_text_colour,
|
||||
col2_date_colour,
|
||||
col2_hours_colour
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
cal_weekday_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='cal_weekday_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:cal_weekday_colour|display_name',
|
||||
'Weekdays'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:cal_weekday_colour|desc',
|
||||
"Colour of the week day letters."
|
||||
),
|
||||
)
|
||||
|
||||
cal_number_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='cal_number_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:cal_number_colour|display_name',
|
||||
'Numbers'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:cal_number_colour|desc',
|
||||
"General calender day colour."
|
||||
),
|
||||
)
|
||||
|
||||
cal_number_end_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='cal_number_end_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:cal_number_end_colour|display_name',
|
||||
'Streak Ends'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:cal_number_end_colour|desc',
|
||||
"Day colour where streaks start or end."
|
||||
),
|
||||
)
|
||||
|
||||
cal_streak_middle_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='cal_streak_middle_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:cal_streak_middle_colour|display_name',
|
||||
'Streak BG'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:cal_streak_middle_colour|desc',
|
||||
"Background colour on streak days."
|
||||
),
|
||||
)
|
||||
|
||||
cal_streak_end_colour = ColourSetting(
|
||||
card=StatsCard,
|
||||
property_name='cal_streak_end_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:cal_streak_end_colour|display_name',
|
||||
'Streak End BG'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:cal_streak_end_colour|desc',
|
||||
"Background colour where streaks start/end."
|
||||
),
|
||||
)
|
||||
|
||||
calender_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:stats|grp:calender_colour', "Calender Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|grp:calender_colour|desc',
|
||||
"Number and streak colours for the current calender."
|
||||
),
|
||||
custom_id='stats-cal',
|
||||
settings=(
|
||||
cal_weekday_colour,
|
||||
cal_number_colour,
|
||||
cal_number_end_colour,
|
||||
cal_streak_middle_colour,
|
||||
cal_streak_end_colour
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
base_skin = SkinSetting(
|
||||
card=StatsCard,
|
||||
property_name='base_skin_id',
|
||||
display_name=_p(
|
||||
'skinsettings|page:stats|set:base_skin|display_name',
|
||||
'Skin'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|set:base_skin|desc',
|
||||
"Select a Skin Preset."
|
||||
)
|
||||
)
|
||||
|
||||
base_skin_group = SettingGroup(
|
||||
_p('skinsettings|page:stats|grp:base_skin', "Statistics Skin"),
|
||||
description=_p(
|
||||
'skinsettings|page:stats|grp:base_skin|desc',
|
||||
"Asset pack and default values for this card."
|
||||
),
|
||||
custom_id='stats-skin',
|
||||
settings=(base_skin,),
|
||||
ungrouped=True
|
||||
)
|
||||
|
||||
stats_page.groups = [base_skin_group, text_colour_group, calender_colour_group]
|
||||
|
||||
89
src/modules/skins/editor/pages/summary.py
Normal file
89
src/modules/skins/editor/pages/summary.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from gui.cards import ProfileCard
|
||||
|
||||
from ... import babel
|
||||
from ..skinsetting import ColourSetting, SkinSetting, ColoursSetting
|
||||
from ..layout import Page, SettingGroup
|
||||
|
||||
from . import stats, profile
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
summary_page = Page(
|
||||
display_name=_p('skinsettings|page:summary|display_name', "Setting Summary"),
|
||||
editing_description=_p(
|
||||
'skinsettings|page:summary|edit_desc',
|
||||
"Simple setup for creating a unified interface theme."
|
||||
),
|
||||
preview_description=_p(
|
||||
'skinsettings|page:summary|preview_desc',
|
||||
"Summary of common settings across the entire interface."
|
||||
),
|
||||
visible_in_preview=True,
|
||||
render_card=ProfileCard
|
||||
)
|
||||
|
||||
name_colours = ColoursSetting(
|
||||
profile.header_colour_1,
|
||||
display_name=_p(
|
||||
'skinsettings|page:summary|set:name_colours|display_name',
|
||||
"username colour"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:summary|set:name_colours|desc',
|
||||
"Author username colour."
|
||||
)
|
||||
)
|
||||
|
||||
discrim_colours = ColoursSetting(
|
||||
profile.header_colour_2,
|
||||
display_name=_p(
|
||||
'skinsettings|page:summary|set:discrim_colours|display_name',
|
||||
"discrim colour"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:summary|set:discrim_colours|desc',
|
||||
"Author discriminator colour."
|
||||
)
|
||||
)
|
||||
|
||||
subheader_colour = ColoursSetting(
|
||||
stats.header_colour,
|
||||
profile.subheader_colour,
|
||||
display_name=_p(
|
||||
'skinsettings|page:summary|set:subheader_colour|display_name',
|
||||
"subheadings"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:summary|set:subheader_colour|desc',
|
||||
"Colour of subheadings and column headings."
|
||||
)
|
||||
)
|
||||
|
||||
header_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:summary|grp:header_colour', "Title Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:summary|grp:header_colour|desc',
|
||||
"Title and header text colours."
|
||||
),
|
||||
custom_id='shared-titles',
|
||||
settings=(
|
||||
subheader_colour,
|
||||
)
|
||||
)
|
||||
|
||||
profile_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:summary|grp:profile_colour', "Profile Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:summary|grp:profile_colour|desc',
|
||||
"Profile elements shared across various cards."
|
||||
),
|
||||
custom_id='shared-profile',
|
||||
settings=(
|
||||
name_colours,
|
||||
discrim_colours
|
||||
)
|
||||
)
|
||||
|
||||
summary_page.groups = [header_colour_group, profile_colour_group]
|
||||
|
||||
350
src/modules/skins/editor/pages/weekly.py
Normal file
350
src/modules/skins/editor/pages/weekly.py
Normal file
@@ -0,0 +1,350 @@
|
||||
from gui.cards import WeeklyStatsCard
|
||||
|
||||
from ... import babel
|
||||
from ..skinsetting import ColourSetting, SkinSetting, ColoursSetting
|
||||
from ..layout import Page, SettingGroup
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
weekly_page = Page(
|
||||
display_name=_p('skinsettings|page:weekly|display_name', "Weekly Statistics"),
|
||||
editing_description=_p(
|
||||
'skinsettings|page:weekly|edit_desc',
|
||||
"Options for the weekly statistis card."
|
||||
),
|
||||
preview_description=None,
|
||||
visible_in_preview=True,
|
||||
render_card=WeeklyStatsCard
|
||||
)
|
||||
|
||||
title_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='title_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:title_colour|display_name',
|
||||
'Title'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:title_colour|desc',
|
||||
"Colour of the card title."
|
||||
)
|
||||
)
|
||||
top_hours_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='top_hours_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:top_hours_colour|display_name',
|
||||
'Hours'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:top_hours_colour|desc',
|
||||
"Hours axis labels."
|
||||
)
|
||||
)
|
||||
top_hours_bg_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='top_hours_bg_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:top_hours_bg_colour|display_name',
|
||||
'Hour Bg'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:top_hours_bg_colour|desc',
|
||||
"Hours axis label background."
|
||||
)
|
||||
)
|
||||
top_line_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='top_line_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:top_line_colour|display_name',
|
||||
'Lines'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:top_line_colour|desc',
|
||||
"Horizontal graph lines."
|
||||
)
|
||||
)
|
||||
top_weekday_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='top_weekday_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:top_weekday_colour|display_name',
|
||||
'Weekdays'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:top_weekday_colour|desc',
|
||||
"Weekday axis labels."
|
||||
)
|
||||
)
|
||||
top_date_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='top_date_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:top_date_colour|display_name',
|
||||
'Dates'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:top_date_colour|desc',
|
||||
"Weekday date axis labels."
|
||||
)
|
||||
)
|
||||
top_this_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='top_this_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:top_this_colour|display_name',
|
||||
'This Week'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:top_this_colour|desc',
|
||||
"This week bar fill colour."
|
||||
)
|
||||
)
|
||||
top_last_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='top_last_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:top_last_colour|display_name',
|
||||
'Last Week'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:top_last_colour|desc',
|
||||
"Last week bar fill colour."
|
||||
)
|
||||
)
|
||||
btm_weekday_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='btm_weekday_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:btm_weekday_colour|display_name',
|
||||
'Weekdays'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:btm_weekday_colour|desc',
|
||||
"Weekday axis labels."
|
||||
)
|
||||
)
|
||||
btm_weekly_background_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='btm_weekly_background_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:btm_weekly_background_colour|display_name',
|
||||
'Weekday Bg'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:btm_weekly_background_colour|desc',
|
||||
"Weekday axis background."
|
||||
)
|
||||
)
|
||||
btm_bar_horiz_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='btm_bar_horiz_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:btm_bar_horiz_colour|display_name',
|
||||
'Bars (Horiz)'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:btm_bar_horiz_colour|desc',
|
||||
"Horizontal graph bars."
|
||||
)
|
||||
)
|
||||
btm_bar_vert_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='btm_bar_vert_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:btm_bar_vert_colour|display_name',
|
||||
'Bars (Vertical)'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:btm_bar_vert_colour|desc',
|
||||
"Vertical graph bars."
|
||||
)
|
||||
)
|
||||
btm_this_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='btm_this_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:btm_this_colour|display_name',
|
||||
'This Week'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:btm_this_colour|desc',
|
||||
"This week bar fill colour."
|
||||
)
|
||||
)
|
||||
btm_last_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='btm_last_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:btm_last_colour|display_name',
|
||||
'Last Week'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:btm_last_colour|desc',
|
||||
"Last week bar fill colour."
|
||||
)
|
||||
)
|
||||
btm_day_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='btm_day_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:btm_day_colour|display_name',
|
||||
'Hours'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:btm_day_colour|desc',
|
||||
"Hour axis labels."
|
||||
)
|
||||
)
|
||||
this_week_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='this_week_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:this_week_colour|display_name',
|
||||
'This Week Legend'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:this_week_colour|desc',
|
||||
"This week legend text."
|
||||
)
|
||||
)
|
||||
last_week_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='last_week_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:last_week_colour|display_name',
|
||||
'Last Week Legend'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:last_week_colour|desc',
|
||||
"Last week legend text."
|
||||
)
|
||||
)
|
||||
legend_colour = ColoursSetting(
|
||||
this_week_colour,
|
||||
last_week_colour,
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:legend_colour|display_name',
|
||||
'Legend'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:legend_colour|desc',
|
||||
"Legend text colour."
|
||||
)
|
||||
)
|
||||
footer_colour = ColourSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='footer_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:footer_colour|display_name',
|
||||
'Footer'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:footer_colour|desc',
|
||||
"Footer text colour."
|
||||
)
|
||||
)
|
||||
|
||||
base_skin = SkinSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='base_skin_id',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:base_skin|display_name',
|
||||
'Skin'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:base_skin|desc',
|
||||
"Select a Skin Preset."
|
||||
)
|
||||
)
|
||||
|
||||
base_skin_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly|grp:base_skin', "Weekly Stats Skin"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|grp:base_skin|desc',
|
||||
"Asset pack and default values for this card."
|
||||
),
|
||||
custom_id='weekly-skin',
|
||||
settings=(base_skin,),
|
||||
ungrouped=True
|
||||
)
|
||||
|
||||
top_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly|grp:top_colour', "Top Graph"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|grp:top_colour|desc',
|
||||
"Customise the top graph colourscheme."
|
||||
),
|
||||
custom_id='weekly-top',
|
||||
settings=(
|
||||
top_hours_colour,
|
||||
top_weekday_colour,
|
||||
top_date_colour,
|
||||
top_this_colour,
|
||||
top_last_colour,
|
||||
)
|
||||
)
|
||||
|
||||
bottom_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly|grp:bottom_colour', "Bottom Graph"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|grp:bottom_colour|desc',
|
||||
"Customise the bottom graph colourscheme."
|
||||
),
|
||||
custom_id='weekly-bottom',
|
||||
settings=(
|
||||
btm_weekday_colour,
|
||||
btm_day_colour,
|
||||
btm_this_colour,
|
||||
btm_last_colour,
|
||||
btm_bar_horiz_colour,
|
||||
)
|
||||
)
|
||||
|
||||
misc_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly|grp:misc', "Misc Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|grp:misc|desc',
|
||||
"Miscellaneous card colours."
|
||||
),
|
||||
custom_id='weekly-misc',
|
||||
settings=(
|
||||
title_colour,
|
||||
legend_colour,
|
||||
footer_colour,
|
||||
)
|
||||
)
|
||||
|
||||
base_skin = SkinSetting(
|
||||
card=WeeklyStatsCard,
|
||||
property_name='base_skin_id',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly|set:base_skin|display_name',
|
||||
'Skin'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|set:base_skin|desc',
|
||||
"Select a Skin Preset."
|
||||
)
|
||||
)
|
||||
|
||||
base_skin_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly|grp:base_skin', "Weekly Stats Skin"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly|grp:base_skin|desc',
|
||||
"Asset pack and default values for the Weekly Statistics."
|
||||
),
|
||||
custom_id='weekly-skin',
|
||||
settings=(base_skin,),
|
||||
ungrouped=True
|
||||
)
|
||||
|
||||
weekly_page.groups = [
|
||||
base_skin_group,
|
||||
top_colour_group,
|
||||
bottom_colour_group,
|
||||
misc_group,
|
||||
]
|
||||
|
||||
438
src/modules/skins/editor/pages/weekly_goals.py
Normal file
438
src/modules/skins/editor/pages/weekly_goals.py
Normal file
@@ -0,0 +1,438 @@
|
||||
from gui.cards import WeeklyGoalCard
|
||||
|
||||
from ... import babel
|
||||
from ..skinsetting import ColourSetting, SkinSetting, ColoursSetting
|
||||
from ..layout import Page, SettingGroup
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
"""
|
||||
mini_profile_name_colour
|
||||
mini_profile_badge_colour
|
||||
mini_profile_badge_text_colour
|
||||
mini_profile_discrim_colour
|
||||
|
||||
title_colour
|
||||
footer_colour
|
||||
|
||||
progress_bg_colour
|
||||
progress_colour
|
||||
[attendance_rate_colour, task_count_colour, studied_hour_colour]
|
||||
[attendance_colour, task_done_colour, studied_text_colour, task_goal_colour]
|
||||
task_goal_number_colour
|
||||
|
||||
task_header_colour
|
||||
task_done_number_colour
|
||||
task_undone_number_colour
|
||||
task_done_text_colour
|
||||
task_undone_text_colour
|
||||
"""
|
||||
|
||||
weekly_goal_page = Page(
|
||||
display_name=_p('skinsettings|page:weekly_goal|display_name', "Weekly Goals"),
|
||||
editing_description=_p(
|
||||
'skinsettings|page:weekly_goal|edit_desc',
|
||||
"Options for the weekly goal card."
|
||||
),
|
||||
preview_description=None,
|
||||
visible_in_preview=True,
|
||||
render_card=WeeklyGoalCard
|
||||
)
|
||||
|
||||
title_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='title_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:title_colour|display_name',
|
||||
"Title"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:title_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
progress_bg_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='progress_bg_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:progress_bg_colour|display_name',
|
||||
"Bar Bg"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:progress_bg_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
progress_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='progress_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:progress_colour|display_name',
|
||||
"Bar Fg"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:progress_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
attendance_rate_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='attendance_rate_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:attendance_rate_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:attendance_rate_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
attendance_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='attendance_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:attendance_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:attendance_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_count_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='task_count_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_count_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_count_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_done_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='task_done_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_done_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_done_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_goal_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='task_goal_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_goal_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_goal_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_goal_number_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='task_goal_number_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_goal_number_colour|display_name',
|
||||
"Task Goal"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_goal_number_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
studied_text_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='studied_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:studied_text_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:studied_text_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
studied_hour_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='studied_hour_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:studied_hour_colour|display_name',
|
||||
""
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:studied_hour_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
text_highlight_colour = ColoursSetting(
|
||||
attendance_rate_colour,
|
||||
task_count_colour,
|
||||
studied_hour_colour,
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:text_highlight_colour|display_name',
|
||||
"Highlight Text"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:text_highlight_colour|desc',
|
||||
"Progress text colour."
|
||||
)
|
||||
)
|
||||
|
||||
text_colour = ColoursSetting(
|
||||
attendance_colour,
|
||||
task_done_colour,
|
||||
studied_text_colour,
|
||||
task_goal_colour,
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:text_colour|display_name',
|
||||
"Text"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:text_colour|desc',
|
||||
"Achievement description text colour."
|
||||
)
|
||||
)
|
||||
|
||||
task_header_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='task_header_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_header_colour|display_name',
|
||||
"Task Header"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_header_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_done_number_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='task_done_number_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_done_number_colour|display_name',
|
||||
"Checked Number"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_done_number_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_undone_number_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='task_undone_number_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_undone_number_colour|display_name',
|
||||
"Unchecked Number"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_undone_number_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_done_text_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='task_done_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_done_text_colour|display_name',
|
||||
"Checked Text"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_done_text_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
task_undone_text_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='task_undone_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_undone_text_colour|display_name',
|
||||
"Unchecked Text"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:task_undone_text_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
footer_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='footer_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:footer_colour|display_name',
|
||||
"Footer"
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:footer_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
mini_profile_badge_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='mini_profile_badge_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:mini_profile_badge_colour|display_name',
|
||||
'Badge Background'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:mini_profile_badge_colour|desc',
|
||||
"Mini-profile badge background colour."
|
||||
)
|
||||
)
|
||||
|
||||
mini_profile_badge_text_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='mini_profile_badge_text_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:mini_profile_badge_text_colour|display_name',
|
||||
'Badge Text'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:mini_profile_badge_text_colour|desc',
|
||||
""
|
||||
)
|
||||
)
|
||||
|
||||
mini_profile_name_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='mini_profile_name_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:mini_profile_name_colour|display_name',
|
||||
'Username'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:mini_profile_name_colour|desc',
|
||||
"Mini-profile username colour."
|
||||
)
|
||||
)
|
||||
|
||||
mini_profile_discrim_colour = ColourSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='mini_profile_discrim_colour',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:mini_profile_discrim_colour|display_name',
|
||||
'Discriminator'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:mini_profile_discrim_colour|desc',
|
||||
"Mini-profile discriminator colour."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
mini_profile_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly_goal|grp:mini_profile', "Profile"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|grp:mini_profile|desc',
|
||||
"Customise the mini-profile shown above the goals."
|
||||
),
|
||||
custom_id='weeklygoal-profile',
|
||||
settings=(
|
||||
mini_profile_name_colour,
|
||||
mini_profile_discrim_colour,
|
||||
mini_profile_badge_colour,
|
||||
mini_profile_badge_text_colour,
|
||||
)
|
||||
)
|
||||
|
||||
misc_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly_goal|grp:misc', "Miscellaneous"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|grp:misc|desc',
|
||||
"Other miscellaneous colours."
|
||||
),
|
||||
custom_id='weeklygoal-misc',
|
||||
settings=(
|
||||
title_colour,
|
||||
footer_colour,
|
||||
)
|
||||
)
|
||||
|
||||
task_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly_goal|grp:task_colour', "Task colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|grp:task_colour|desc',
|
||||
"Text and number colours for (in)complete goals."
|
||||
),
|
||||
custom_id='weeklygoal-tasks',
|
||||
settings=(
|
||||
task_undone_number_colour,
|
||||
task_done_number_colour,
|
||||
task_undone_text_colour,
|
||||
task_done_text_colour,
|
||||
)
|
||||
)
|
||||
|
||||
progress_colour_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly_goal|grp:progress_colour', "Progress Colours"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|grp:progress_colour|desc',
|
||||
"Customise colours for the weekly achievement progress."
|
||||
),
|
||||
custom_id='weeklygoal-progress',
|
||||
settings=(
|
||||
progress_bg_colour,
|
||||
progress_colour,
|
||||
text_colour,
|
||||
text_highlight_colour,
|
||||
task_goal_number_colour
|
||||
)
|
||||
)
|
||||
|
||||
base_skin = SkinSetting(
|
||||
card=WeeklyGoalCard,
|
||||
property_name='base_skin_id',
|
||||
display_name=_p(
|
||||
'skinsettings|page:weekly_goal|set:base_skin|display_name',
|
||||
'Skin'
|
||||
),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|set:base_skin|desc',
|
||||
"Select a Skin Preset."
|
||||
)
|
||||
)
|
||||
|
||||
base_skin_group = SettingGroup(
|
||||
_p('skinsettings|page:weekly_goal|grp:base_skin', "Weekly Goals Skin"),
|
||||
description=_p(
|
||||
'skinsettings|page:weekly_goal|grp:base_skin|desc',
|
||||
"Asset pack and default values for the Weekly Goals."
|
||||
),
|
||||
custom_id='weeklygoals-skin',
|
||||
settings=(base_skin,),
|
||||
ungrouped=True
|
||||
)
|
||||
|
||||
|
||||
weekly_goal_page.groups = [
|
||||
base_skin_group,
|
||||
mini_profile_group,
|
||||
misc_group,
|
||||
progress_colour_group,
|
||||
task_colour_group
|
||||
]
|
||||
|
||||
598
src/modules/skins/editor/skineditor.py
Normal file
598
src/modules/skins/editor/skineditor.py
Normal file
@@ -0,0 +1,598 @@
|
||||
from io import StringIO
|
||||
import json
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
|
||||
import discord
|
||||
from discord.ui.button import button, Button, ButtonStyle
|
||||
from discord.ui.select import select, Select, SelectOption
|
||||
from gui.base.AppSkin import AppSkin
|
||||
|
||||
from meta import LionBot, conf
|
||||
from meta.errors import ResponseTimedOut, UserInputError
|
||||
from meta.logger import log_wrap
|
||||
from utils.ui import FastModal, Confirm, MessageUI, error_handler_for, ModalRetryUI, AButton, AsComponents
|
||||
from utils.lib import MessageArgs, utc_now
|
||||
from constants import DATA_VERSION
|
||||
|
||||
from .. import babel, logger
|
||||
from ..skinlib import CustomSkin, FrozenCustomSkin, appskin_as_option
|
||||
from .pages import pages
|
||||
from .skinsetting import Setting, SettingInputType, SkinSetting
|
||||
from .layout import SettingGroup, Page
|
||||
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class SettingInput(FastModal):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@error_handler_for(UserInputError)
|
||||
async def rerequest(self, interaction, error):
|
||||
await ModalRetryUI(self, error.msg).respond_to(interaction)
|
||||
|
||||
|
||||
class CustomSkinEditor(MessageUI):
|
||||
def _init_children(self):
|
||||
# HACK to stop ViewWeights complaining that this UI has too many children
|
||||
# Children will be correctly initialised after parent init.
|
||||
return []
|
||||
|
||||
def __init__(self, skin: CustomSkin, **kwargs):
|
||||
super().__init__(timeout=600, **kwargs)
|
||||
self._children = super()._init_children()
|
||||
|
||||
self.skin = skin
|
||||
self.bot = skin.bot
|
||||
self.cog = self.bot.get_cog('CustomSkinCog')
|
||||
|
||||
self.global_themes = self._get_available()
|
||||
|
||||
# UI State
|
||||
|
||||
# Whether we are currently in customisation mode
|
||||
self.customising = False
|
||||
self.page_index = 0
|
||||
self.showing_skin_setting: Optional[SkinSetting] = None
|
||||
|
||||
# Last item in history is current state
|
||||
# Last item in future is next state
|
||||
self.history = [skin.freeze()]
|
||||
self.future = []
|
||||
self.dirty = False
|
||||
|
||||
@property
|
||||
def page(self) -> Page:
|
||||
return pages[self.page_index]
|
||||
|
||||
# ----- UI API -----
|
||||
def push_state(self):
|
||||
"""
|
||||
Push a state onto the history stack.
|
||||
Run this on each change _before_ the refresh.
|
||||
"""
|
||||
state = self.skin.freeze()
|
||||
self.history.append(state)
|
||||
self.future.clear()
|
||||
self.dirty = True
|
||||
|
||||
def _get_available(self) -> dict[str, AppSkin]:
|
||||
skins = {
|
||||
skin.skin_id: skin for skin in AppSkin.get_all()
|
||||
if skin.public
|
||||
}
|
||||
skins['default'] = self._make_default()
|
||||
return skins
|
||||
|
||||
def _make_default(self) -> AppSkin:
|
||||
"""
|
||||
Create a placeholder 'default' skin.
|
||||
"""
|
||||
t = self.bot.translator.t
|
||||
|
||||
skin = AppSkin(None)
|
||||
skin.skin_id = 'default'
|
||||
skin.display_name = t(_p(
|
||||
'ui:skineditor|default_skin:display_name',
|
||||
"Default"
|
||||
))
|
||||
skin.description = t(_p(
|
||||
'ui:skineditor|default_skin:description',
|
||||
"My default interface theme"
|
||||
))
|
||||
skin.price = 0
|
||||
return skin
|
||||
|
||||
# ----- UI Components -----
|
||||
|
||||
# Download button
|
||||
# NOTE: property_id, card_id, property_name, value
|
||||
# Metadata with version, time generated, skinid, generating user
|
||||
# Special field for the global_skin_id
|
||||
@button(
|
||||
label="DOWNLOAD_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.blurple
|
||||
)
|
||||
async def download_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
data = {}
|
||||
data['metadata'] = {
|
||||
'requested_by': press.user.id,
|
||||
'requested_in': press.guild.id if press.guild else None,
|
||||
'skinid': self.skin.skinid,
|
||||
'created_at': utc_now().isoformat(),
|
||||
'data_version': DATA_VERSION,
|
||||
}
|
||||
data['custom_skin'] = {
|
||||
'skinid': self.skin.skinid,
|
||||
'base_skin': self.skin.base_skin_name,
|
||||
}
|
||||
properties = {}
|
||||
for card, card_props in self.skin.properties.items():
|
||||
props = {}
|
||||
for name, value in card_props.items():
|
||||
propid = (await self.cog.fetch_property_ids((card, name)))[0]
|
||||
props[name] = {
|
||||
'property_id': propid,
|
||||
'value': value
|
||||
}
|
||||
properties[card] = props
|
||||
data['custom_skin']['properties'] = properties
|
||||
|
||||
content = json.dumps(data, indent=2)
|
||||
with StringIO(content) as fp:
|
||||
fp.seek(0)
|
||||
file = discord.File(fp, filename=f"skin-{self.skin.skinid}.json")
|
||||
await press.followup.send("Here is your custom skin data!", file=file, ephemeral=True)
|
||||
|
||||
async def download_button_refresh(self):
|
||||
button = self.download_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:skineditor|button:download|label',
|
||||
"Download"
|
||||
))
|
||||
|
||||
# Save button
|
||||
@button(
|
||||
label="SAVE_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.green
|
||||
)
|
||||
async def save_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
await self.skin.save()
|
||||
self.history = self.history[-1:]
|
||||
self.dirty = False
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
async def save_button_refresh(self):
|
||||
button = self.save_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:skineditor|button:save|label',
|
||||
"Save"
|
||||
))
|
||||
button.disabled = not self.dirty
|
||||
|
||||
# Back button
|
||||
@button(
|
||||
label="BACK_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.red
|
||||
)
|
||||
async def back_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
self.customising = False
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
async def back_button_refresh(self):
|
||||
button = self.back_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:skineditor|button:back|label',
|
||||
"Back"
|
||||
))
|
||||
|
||||
# Customise button
|
||||
@button(
|
||||
label="CUSTOMISE_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.green
|
||||
)
|
||||
async def customise_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
self.customising = True
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
async def customise_button_refresh(self):
|
||||
button = self.customise_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:skineditor|button:customise|label',
|
||||
"Customise"
|
||||
))
|
||||
|
||||
# Reset card button
|
||||
@button(
|
||||
label="RESET_CARD_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.red
|
||||
)
|
||||
async def reset_card_button(self, press: discord.Interaction, pressed: Button):
|
||||
# Note this actually resets the page, not the card
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
for group in self.page.groups:
|
||||
for setting in group.settings:
|
||||
setting.set_in(self.skin, None)
|
||||
|
||||
self.push_state()
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
async def reset_card_button_refresh(self):
|
||||
button = self.reset_card_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:skineditor|button:reset_card|label',
|
||||
"Reset Card"
|
||||
))
|
||||
|
||||
# Reset all button
|
||||
@button(
|
||||
label="RESET_ALL_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.red
|
||||
)
|
||||
async def reset_all_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
self.skin.properties.clear()
|
||||
self.skin.base_skin_name = None
|
||||
|
||||
self.push_state()
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
async def reset_all_button_refresh(self):
|
||||
button = self.reset_all_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:skineditor|button:reset_all|label',
|
||||
"Reset All"
|
||||
))
|
||||
|
||||
# Page selector
|
||||
@select(
|
||||
cls=Select,
|
||||
placeholder="PAGE_MENU_PLACEHOLDER",
|
||||
min_values=1, max_values=1
|
||||
)
|
||||
async def page_menu(self, selection: discord.Interaction, selected: Select):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
self.page_index = int(selected.values[0])
|
||||
self.showing_skin_setting = None
|
||||
await self.refresh(thinking=selection)
|
||||
|
||||
async def page_menu_refresh(self):
|
||||
menu = self.page_menu
|
||||
t = self.bot.translator.t
|
||||
|
||||
options = []
|
||||
if not self.customising:
|
||||
menu.placeholder = t(_p(
|
||||
'ui:skineditor|menu:page|placeholder:preview',
|
||||
"Select a card to preview"
|
||||
))
|
||||
for i, page in enumerate(pages):
|
||||
if page.visible_in_preview:
|
||||
option = SelectOption(
|
||||
label=t(page.display_name),
|
||||
value=str(i),
|
||||
description=t(page.preview_description) if page.preview_description else None
|
||||
)
|
||||
option.default = (i == self.page_index)
|
||||
options.append(option)
|
||||
else:
|
||||
menu.placeholder = t(_p(
|
||||
'ui:skineditor|menu:page|placeholder:edit',
|
||||
"Select a card to customise"
|
||||
))
|
||||
for i, page in enumerate(pages):
|
||||
option = SelectOption(
|
||||
label=t(page.display_name),
|
||||
value=str(i),
|
||||
description=t(page.editing_description) if page.editing_description else None
|
||||
)
|
||||
option.default = (i == self.page_index)
|
||||
options.append(option)
|
||||
menu.options = options
|
||||
|
||||
# Setting group selector
|
||||
@select(
|
||||
cls=Select,
|
||||
placeholder="GROUP_MENU_PLACEHOLDER",
|
||||
min_values=1, max_values=1
|
||||
)
|
||||
async def group_menu(self, selection: discord.Interaction, selected: Select):
|
||||
groupid = selected.values[0]
|
||||
group = next(group for group in self.page.groups if group.custom_id == groupid)
|
||||
|
||||
if group.settings[0].input_type is SettingInputType.SkinInput:
|
||||
self.showing_skin_setting = group.settings[0]
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
await self.refresh(thinking=selection)
|
||||
else:
|
||||
await self._launch_group_editor(selection, group)
|
||||
|
||||
async def _launch_group_editor(self, interaction: discord.Interaction, group: SettingGroup):
|
||||
t = self.bot.translator.t
|
||||
|
||||
editable = group.editable_settings
|
||||
items = [
|
||||
setting.make_input_field(self.skin)
|
||||
for setting in editable
|
||||
]
|
||||
modal = SettingInput(*items, title=t(group.name))
|
||||
|
||||
@modal.submit_callback()
|
||||
async def group_modal_callback(interaction: discord.Interaction):
|
||||
values = []
|
||||
for item, setting in zip(items, editable):
|
||||
value = await setting.parse_input(self.skin, item.value)
|
||||
values.append(value)
|
||||
|
||||
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||
for value, setting in zip(values, editable):
|
||||
setting.set_in(self.skin, value)
|
||||
|
||||
self.push_state()
|
||||
await self.refresh(thinking=interaction)
|
||||
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
async def group_menu_refresh(self):
|
||||
menu = self.group_menu
|
||||
t = self.bot.translator.t
|
||||
menu.placeholder = t(_p(
|
||||
'ui:skineditor|menu:group|placeholder',
|
||||
"Select a group or option to customise"
|
||||
))
|
||||
options = []
|
||||
for group in self.page.groups:
|
||||
option = group.select_option_for(self.skin)
|
||||
options.append(option)
|
||||
menu.options = options
|
||||
|
||||
# Base skin selector
|
||||
@select(
|
||||
cls=Select,
|
||||
placeholder="SKIN_MENU_PLACEHOLDER",
|
||||
min_values=1, max_values=1
|
||||
)
|
||||
async def skin_menu(self, selection: discord.Interaction, selected: Select):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
skin_id = selected.values[0]
|
||||
if skin_id == 'default':
|
||||
skin_id = None
|
||||
|
||||
if self.customising:
|
||||
if self.showing_skin_setting:
|
||||
# Update the current page card with this skin id.
|
||||
self.showing_skin_setting.set_in(self.skin, skin_id)
|
||||
else:
|
||||
# Far more brutal
|
||||
# Update the global base skin id, and wipe the base skin id for each card
|
||||
self.skin.base_skin_name = skin_id
|
||||
for card_id in self.skin.properties:
|
||||
self.skin.set_prop(card_id, 'base_skin_id', None)
|
||||
|
||||
self.push_state()
|
||||
await self.refresh(thinking=selection)
|
||||
|
||||
async def skin_menu_refresh(self):
|
||||
menu = self.skin_menu
|
||||
t = self.bot.translator.t
|
||||
menu.placeholder = t(_p(
|
||||
'ui:skineditor|menu:skin|placeholder',
|
||||
"Select a theme"
|
||||
))
|
||||
options = []
|
||||
for skin in self.global_themes.values():
|
||||
option = appskin_as_option(skin)
|
||||
options.append(option)
|
||||
menu.options = options
|
||||
|
||||
# Quit button, with confirmation
|
||||
@button(style=ButtonStyle.grey, emoji=conf.emojis.cancel)
|
||||
async def quit_button(self, press: discord.Interaction, pressed: Button):
|
||||
# Confirm quit if there are unsaved changes
|
||||
if self.dirty:
|
||||
t = self.bot.translator.t
|
||||
confirm_msg = t(_p(
|
||||
'ui:skineditor|button:quit|confirm',
|
||||
"You have unsaved changes! Are you sure you want to quit?"
|
||||
))
|
||||
confirm = Confirm(confirm_msg, self._callerid)
|
||||
confirm.confirm_button.label = t(_p(
|
||||
'ui:skineditor|button:quit|confirm|button:yes',
|
||||
"Yes, Quit Now"
|
||||
))
|
||||
confirm.confirm_button.style = ButtonStyle.red
|
||||
confirm.cancel_button.style = ButtonStyle.green
|
||||
confirm.cancel_button.label = t(_p(
|
||||
'ui:skineditor|button:quit|confirm|button:no',
|
||||
"No, Go Back"
|
||||
))
|
||||
try:
|
||||
result = await confirm.ask(press, ephemeral=True)
|
||||
except ResponseTimedOut:
|
||||
result = False
|
||||
|
||||
if result:
|
||||
await self.quit()
|
||||
else:
|
||||
await self.quit()
|
||||
|
||||
@button(label="UNDO_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
||||
async def undo_button(self, press: discord.Interaction, pressed: Button):
|
||||
"""
|
||||
Pop the history stack.
|
||||
"""
|
||||
if len(self.history) > 1:
|
||||
state = self.history.pop()
|
||||
self.future.append(state)
|
||||
|
||||
current = self.history[-1]
|
||||
self.skin.load_frozen(current)
|
||||
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
async def undo_button_refresh(self):
|
||||
t = self.bot.translator.t
|
||||
button = self.undo_button
|
||||
button.label = t(_p(
|
||||
'ui:skineditor|button:undo|label',
|
||||
"Undo"
|
||||
))
|
||||
button.disabled = (len(self.history) <= 1)
|
||||
|
||||
@button(label="REDO_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple)
|
||||
async def redo_button(self, press: discord.Interaction, pressed: Button):
|
||||
"""
|
||||
Pop the future stack.
|
||||
"""
|
||||
if len(self.future) > 0:
|
||||
state = self.future.pop()
|
||||
self.history.append(state)
|
||||
|
||||
current = self.history[-1]
|
||||
self.skin.load_frozen(current)
|
||||
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
async def redo_button_refresh(self):
|
||||
t = self.bot.translator.t
|
||||
button = self.redo_button
|
||||
button.label = t(_p(
|
||||
'ui:skineditor|button:redo|label',
|
||||
"Redo"
|
||||
))
|
||||
button.disabled = (len(self.future) == 0)
|
||||
|
||||
# ----- UI Flow -----
|
||||
async def make_message(self) -> MessageArgs:
|
||||
page = self.page
|
||||
|
||||
embed = page.make_embed_for(self.skin)
|
||||
if page.render_card is not None:
|
||||
args = self.skin.args_for(page.render_card.card_id)
|
||||
args.setdefault('base_skin_id', self.cog.current_default)
|
||||
file = await page.render_card.generate_sample(skin=args)
|
||||
files = [file]
|
||||
else:
|
||||
files = []
|
||||
|
||||
return MessageArgs(embed=embed, files=files)
|
||||
|
||||
async def refresh_layout(self):
|
||||
"""
|
||||
Customising mode:
|
||||
(card_menu)
|
||||
(skin_menu?)
|
||||
(download, save, undo, redo, back,)
|
||||
Other:
|
||||
(card_menu)
|
||||
(theme_menu)
|
||||
(customise, save, reset_card, reset_all, X)
|
||||
"""
|
||||
to_refresh = (
|
||||
self.page_menu_refresh(),
|
||||
self.skin_menu_refresh(),
|
||||
self.undo_button_refresh(),
|
||||
self.redo_button_refresh(),
|
||||
self.reset_card_button_refresh(),
|
||||
self.reset_all_button_refresh(),
|
||||
self.customise_button_refresh(),
|
||||
self.back_button_refresh(),
|
||||
self.save_button_refresh(),
|
||||
self.group_menu_refresh(),
|
||||
self.download_button_refresh(),
|
||||
)
|
||||
await asyncio.gather(*to_refresh)
|
||||
|
||||
if self.customising:
|
||||
self.set_layout(
|
||||
(self.page_menu,),
|
||||
(self.group_menu,),
|
||||
(self.skin_menu,) if self.showing_skin_setting else (),
|
||||
(
|
||||
self.save_button,
|
||||
self.undo_button,
|
||||
self.redo_button,
|
||||
self.download_button,
|
||||
self.back_button,
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.set_layout(
|
||||
(self.page_menu,),
|
||||
(self.skin_menu,),
|
||||
(
|
||||
self.customise_button,
|
||||
self.save_button,
|
||||
self.reset_card_button,
|
||||
self.reset_all_button,
|
||||
self.quit_button,
|
||||
),
|
||||
)
|
||||
|
||||
async def reload(self):
|
||||
...
|
||||
|
||||
async def pre_timeout(self):
|
||||
# Timeout confirmation
|
||||
if self.dirty:
|
||||
t = self.bot.translator.t
|
||||
grace_period = 60
|
||||
grace_time = utc_now() + dt.timedelta(seconds=grace_period)
|
||||
embed = discord.Embed(
|
||||
title=t(_p(
|
||||
'ui:skineditor|timeout_warning|title',
|
||||
"Warning!"
|
||||
)),
|
||||
description=t(_p(
|
||||
'ui:skineditor|timeout_warning|desc',
|
||||
"This interface will time out {timestamp}. Press 'Continue' below to keep editing."
|
||||
)).format(
|
||||
timestamp=discord.utils.format_dt(grace_time, style='R')
|
||||
),
|
||||
)
|
||||
|
||||
components = None
|
||||
stopped = False
|
||||
|
||||
@AButton(label=t(_p('ui:skineditor|timeout_warning|continue', "Continue")), style=ButtonStyle.green)
|
||||
async def cont_button(interaction: discord.Interaction, pressed):
|
||||
await interaction.response.defer()
|
||||
if interaction.message:
|
||||
await interaction.message.delete()
|
||||
nonlocal stopped
|
||||
stopped = True
|
||||
# TODO: Clean up this mess. It works, but needs to be refactored to a timeout confirmation mixin.
|
||||
# TODO: Consider moving the message to the interaction response
|
||||
self._refresh_timeout()
|
||||
components.stop()
|
||||
|
||||
components = AsComponents(cont_button, timeout=grace_period)
|
||||
channel = self._original.channel if self._original else self._message.channel
|
||||
|
||||
message = await channel.send(content=f"<@{self._callerid}>", embed=embed, view=components)
|
||||
await components.wait()
|
||||
|
||||
if not stopped:
|
||||
try:
|
||||
await message.delete()
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
298
src/modules/skins/editor/skinsetting.py
Normal file
298
src/modules/skins/editor/skinsetting.py
Normal file
@@ -0,0 +1,298 @@
|
||||
from typing import Literal, Optional
|
||||
from enum import Enum
|
||||
|
||||
from discord.ui import TextInput
|
||||
|
||||
from meta import LionBot
|
||||
from meta.errors import UserInputError
|
||||
from babel.translator import LazyStr
|
||||
from gui.base import Card, FieldDesc, AppSkin
|
||||
|
||||
from .. import babel
|
||||
from ..skinlib import CustomSkin
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class SettingInputType(Enum):
|
||||
SkinInput = -1
|
||||
ModalInput = 0
|
||||
MenuInput = 1
|
||||
ButtonInput = 2
|
||||
|
||||
|
||||
class Setting:
|
||||
"""
|
||||
An abstract base interface for a custom skin 'setting'.
|
||||
|
||||
A skin setting is considered to be some readable and usually writeable
|
||||
information extractable from a `CustomSkin`.
|
||||
This will usually consist of the value of one or more properties,
|
||||
which are themselves associated to fields of GUI Cards.
|
||||
|
||||
The methods in this ABC describe the interface for such a setting.
|
||||
Each method accepts a `CustomSkin`,
|
||||
and an implementation should describe how to
|
||||
get, set, parse, format, or display the setting
|
||||
for that given skin.
|
||||
|
||||
This is very similar to how Settings are implemented in the bot,
|
||||
except here all settings have a shared external source of state, the CustomSkin.
|
||||
Thus, each setting is simply an instance of an appropriate setting class,
|
||||
rather than a class itself.
|
||||
"""
|
||||
|
||||
# What type of input method this setting requires for input
|
||||
input_type: SettingInputType = SettingInputType.ModalInput
|
||||
|
||||
def __init__(self, *args, display_name, description, **kwargs):
|
||||
self.display_name: LazyStr = display_name
|
||||
self.description: LazyStr = description
|
||||
|
||||
def default_value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
The default value of this setting in this skin.
|
||||
|
||||
This takes into account base skin data and localisation.
|
||||
May be `None` if the setting does not have a default value.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
The current value of this setting from this skin.
|
||||
|
||||
May be None if the setting is not set or does not have a value.
|
||||
Usually should not take into account defaults.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_in(self, skin: CustomSkin, value: Optional[str]):
|
||||
"""
|
||||
Set this setting to the given value in this skin.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def format_value_in(self, skin: CustomSkin, value: Optional[str]) -> str:
|
||||
"""
|
||||
Format the given setting value for display (typically in a setting table).
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def parse_input(self, skin: CustomSkin, userstr: str) -> Optional[str]:
|
||||
"""
|
||||
Parse a user provided string into a value for this setting.
|
||||
|
||||
Will raise 'UserInputError' with a readable message if parsing fails.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def make_input_field(self, skin: CustomSkin) -> TextInput:
|
||||
"""
|
||||
Create a TextInput field for this setting, using the current value.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PropertySetting(Setting):
|
||||
"""
|
||||
A skin setting corresponding to a single property of a single card.
|
||||
|
||||
Note that this is still abstract,
|
||||
as it does not implement any formatting or parsing methods.
|
||||
|
||||
This will usually (but may not always) correspond to a single Field of the card skin.
|
||||
"""
|
||||
def __init__(self, card: type[Card], property_name: str, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.card = card
|
||||
self.property_name = property_name
|
||||
|
||||
@property
|
||||
def card_id(self):
|
||||
"""
|
||||
The `card_id` of the Card class this setting belongs to.
|
||||
"""
|
||||
return self.card.card_id
|
||||
|
||||
@property
|
||||
def field(self) -> Optional[FieldDesc]:
|
||||
"""
|
||||
The CardSkin field overwrriten by this setting, if it exists.
|
||||
"""
|
||||
return self.card.skin._fields.get(self.property_name, None)
|
||||
|
||||
def default_value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
For a PropertySetting, the default value is determined as follows:
|
||||
base skin value from:
|
||||
- card base skin
|
||||
- custom base skin
|
||||
- global app base skin
|
||||
fallback (field) value from the CardSkin
|
||||
"""
|
||||
base_skin = skin.get_prop(self.card_id, 'base_skin_id')
|
||||
base_skin = base_skin or skin.base_skin_name
|
||||
base_skin = base_skin or skin.cog.current_default
|
||||
|
||||
app_skin_args = AppSkin.get(base_skin).for_card(self.card_id)
|
||||
|
||||
if self.property_name in app_skin_args:
|
||||
return app_skin_args[self.property_name]
|
||||
elif self.field:
|
||||
return self.field.default
|
||||
else:
|
||||
return None
|
||||
|
||||
def value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
return skin.get_prop(self.card_id, self.property_name)
|
||||
|
||||
def set_in(self, skin: CustomSkin, value: Optional[str]):
|
||||
skin.set_prop(self.card_id, self.property_name, value)
|
||||
|
||||
|
||||
class _ColourInterface(Setting):
|
||||
"""
|
||||
Skin setting mixin for parsing and formatting colour typed settings.
|
||||
"""
|
||||
|
||||
def format_value_in(self, skin: CustomSkin, value: Optional[str]) -> str:
|
||||
if value:
|
||||
formatted = f"`{value}`"
|
||||
else:
|
||||
formatted = skin.bot.translator.t(_p(
|
||||
'skinsettings|colours|format:not_set',
|
||||
"Not Set"
|
||||
))
|
||||
return formatted
|
||||
|
||||
async def parse_input(self, skin: CustomSkin, userstr: str) -> Optional[str]:
|
||||
stripped = userstr.strip('# ').upper()
|
||||
if not stripped:
|
||||
value = None
|
||||
elif len(stripped) not in (6, 8) or any(c not in '0123456789ABCDEF' for c in stripped):
|
||||
raise UserInputError(
|
||||
skin.bot.translator.t(_p(
|
||||
'skinsettings|colours|parse|error:invalid',
|
||||
"Could not parse `{given}` as a colour!"
|
||||
" Please use RGB/RGBA format (e.g. `#ABABABF0`)."
|
||||
)).format(given=userstr)
|
||||
)
|
||||
else:
|
||||
value = f"#{stripped}"
|
||||
return value
|
||||
|
||||
def make_input_field(self, skin: CustomSkin) -> TextInput:
|
||||
t = skin.bot.translator.t
|
||||
|
||||
value = self.value_in(skin)
|
||||
default_value = self.default_value_in(skin)
|
||||
|
||||
label = t(self.display_name)
|
||||
default = value
|
||||
if default_value:
|
||||
placeholder = f"{default_value} ({t(self.description)})"
|
||||
else:
|
||||
placeholder = t(self.description)
|
||||
|
||||
return TextInput(
|
||||
label=label,
|
||||
placeholder=placeholder,
|
||||
default=default,
|
||||
min_length=0,
|
||||
max_length=9,
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class ColourSetting(_ColourInterface, PropertySetting):
|
||||
"""
|
||||
A Property skin setting representing a single colour field.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class SkinSetting(PropertySetting):
|
||||
"""
|
||||
A Property setting representing the base skin of a card.
|
||||
"""
|
||||
input_type = SettingInputType.SkinInput
|
||||
|
||||
def format_value_in(self, skin: CustomSkin, value: Optional[str]) -> str:
|
||||
if value:
|
||||
app_skin = AppSkin.get(value)
|
||||
formatted = f"`{app_skin.display_name}`"
|
||||
else:
|
||||
formatted = skin.bot.translator.t(_p(
|
||||
'skinsettings|base_skin|format:not_set',
|
||||
"Default"
|
||||
))
|
||||
return formatted
|
||||
|
||||
def default_value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
return skin.base_skin_name
|
||||
|
||||
|
||||
class CompoundSetting(Setting):
|
||||
"""
|
||||
A Setting combining several PropertySettings across (potentially) multiple cards.
|
||||
"""
|
||||
NOTSHARED = ''
|
||||
|
||||
def __init__(self, *settings: PropertySetting, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.settings = settings
|
||||
|
||||
def default_value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
The default value of a CompoundSetting is the shared default of the component settings.
|
||||
|
||||
If the components do not share a default value, returns None.
|
||||
"""
|
||||
value = None
|
||||
for setting in self.settings:
|
||||
setting_value = setting.default_value_in(skin)
|
||||
if setting_value is None:
|
||||
value = None
|
||||
break
|
||||
if value is None:
|
||||
value = setting_value
|
||||
elif value != setting_value:
|
||||
value = None
|
||||
break
|
||||
return value
|
||||
|
||||
def value_in(self, skin: CustomSkin) -> Optional[str]:
|
||||
"""
|
||||
The value of a compound setting is the shared value of the components.
|
||||
"""
|
||||
value = self.NOTSHARED
|
||||
for setting in self.settings:
|
||||
setting_value = setting.value_in(skin) or setting.default_value_in(skin)
|
||||
|
||||
if value is self.NOTSHARED:
|
||||
value = setting_value
|
||||
elif value != setting_value:
|
||||
value = self.NOTSHARED
|
||||
break
|
||||
return value
|
||||
|
||||
def set_in(self, skin: CustomSkin, value: Optional[str]):
|
||||
"""
|
||||
Set all of the components individually.
|
||||
"""
|
||||
for setting in self.settings:
|
||||
setting.set_in(skin, value)
|
||||
|
||||
|
||||
class ColoursSetting(_ColourInterface, CompoundSetting):
|
||||
"""
|
||||
Compound setting representing multiple colours.
|
||||
"""
|
||||
def format_value_in(self, skin: CustomSkin, value: Optional[str]) -> str:
|
||||
if value is self.NOTSHARED:
|
||||
return "Mixed"
|
||||
elif value is None:
|
||||
return "Not Set"
|
||||
else:
|
||||
return f"`{value}`"
|
||||
55
src/modules/skins/settings.py
Normal file
55
src/modules/skins/settings.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from meta.errors import UserInputError
|
||||
from settings.data import ModelData
|
||||
from settings.setting_types import StringSetting
|
||||
from settings.groups import SettingGroup
|
||||
|
||||
from wards import sys_admin_iward
|
||||
from core.data import CoreData
|
||||
from gui.base import AppSkin
|
||||
from babel.translator import ctx_translator
|
||||
|
||||
from . import babel
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class GlobalSkinSettings(SettingGroup):
|
||||
class DefaultSkin(ModelData, StringSetting):
|
||||
setting_id = 'default_app_skin'
|
||||
_event = 'botset_skin'
|
||||
_write_ward = sys_admin_iward
|
||||
|
||||
_display_name = _p(
|
||||
'botset:default_app_skin', "default_skin"
|
||||
)
|
||||
_desc = _p(
|
||||
'botset:default_app_skin|desc',
|
||||
"The skin name of the app skin to use as the global default."
|
||||
)
|
||||
_long_desc = _p(
|
||||
'botset:default_app_skin|long_desc',
|
||||
"The skin name, as given in the `skins.json` file,"
|
||||
" of the client default interface skin."
|
||||
" Guilds and users will be able to apply this skin"
|
||||
"regardless of whether it is set as visible in the skin configuration."
|
||||
)
|
||||
_accepts = _p(
|
||||
'botset:default_app_skin|accepts',
|
||||
"A valid skin name as given in skins.json"
|
||||
)
|
||||
|
||||
_model = CoreData.BotConfig
|
||||
_column = CoreData.BotConfig.default_skin.name
|
||||
|
||||
@classmethod
|
||||
async def _parse_string(cls, parent_id, string, **kwargs):
|
||||
t = ctx_translator.get().t
|
||||
if string and not AppSkin.get_skin_path(string):
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'botset:default_app_skin|parse|error:invalid',
|
||||
"Provided `{string}` is not a valid skin id!"
|
||||
)).format(string=string)
|
||||
)
|
||||
return string or None
|
||||
|
||||
100
src/modules/skins/settingui.py
Normal file
100
src/modules/skins/settingui.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ui.select import select, Select
|
||||
|
||||
from utils.ui import ConfigUI
|
||||
from utils.lib import MessageArgs
|
||||
from meta import LionBot
|
||||
|
||||
from . import babel, logger
|
||||
from .settings import GlobalSkinSettings as Settings
|
||||
from .skinlib import appskin_as_option
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class GlobalSkinSettingUI(ConfigUI):
|
||||
setting_classes = (
|
||||
Settings.DefaultSkin,
|
||||
)
|
||||
|
||||
def __init__(self, bot: LionBot, appname: str, channelid: int, **kwargs):
|
||||
self.cog = bot.get_cog('CustomSkinCog')
|
||||
super().__init__(bot, appname, channelid, **kwargs)
|
||||
|
||||
# ----- UI Components -----
|
||||
@select(
|
||||
cls=Select,
|
||||
placeholder="DEFAULT_APP_MENU_PLACEHOLDER",
|
||||
min_values=0, max_values=1
|
||||
)
|
||||
async def default_app_menu(self, selection: discord.Interaction, selected: Select):
|
||||
await selection.response.defer(thinking=False)
|
||||
setting = self.instances[0]
|
||||
|
||||
if selected.values:
|
||||
setting.data = selected.values[0]
|
||||
await setting.write()
|
||||
else:
|
||||
setting.data = None
|
||||
await setting.write()
|
||||
|
||||
async def default_app_menu_refresh(self):
|
||||
menu = self.default_app_menu
|
||||
t = self.bot.translator.t
|
||||
menu.placeholder = t(_p(
|
||||
'ui:appskins|menu:default_app|placeholder',
|
||||
"Select Default Skin"
|
||||
))
|
||||
options = []
|
||||
for skinid in self.cog.appskin_names:
|
||||
appskin = self.cog.get_base(skinid)
|
||||
option = appskin_as_option(appskin)
|
||||
option.default = (
|
||||
self.instances[0].value == appskin.skin_id
|
||||
)
|
||||
options.append(option)
|
||||
if options:
|
||||
menu.options = options
|
||||
else:
|
||||
menu.disabled = True
|
||||
menu.options = [
|
||||
discord.SelectOption(label='DUMMY')
|
||||
]
|
||||
|
||||
# ----- UI Flow -----
|
||||
async def make_message(self) -> MessageArgs:
|
||||
t = self.bot.translator.t
|
||||
title = t(_p(
|
||||
'ui:appskins|embed|title',
|
||||
"Leo Global Skin Settings"
|
||||
))
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
colour=discord.Colour.orange()
|
||||
)
|
||||
for setting in self.instances:
|
||||
embed.add_field(**setting.embed_field, inline=False)
|
||||
|
||||
return MessageArgs(embed=embed)
|
||||
|
||||
async def refresh_components(self):
|
||||
to_refresh = (
|
||||
self.edit_button_refresh(),
|
||||
self.close_button_refresh(),
|
||||
self.reset_button_refresh(),
|
||||
self.default_app_menu_refresh(),
|
||||
)
|
||||
await asyncio.gather(*to_refresh)
|
||||
|
||||
self.set_layout(
|
||||
(self.edit_button, self.reset_button, self.close_button,),
|
||||
(self.default_app_menu,),
|
||||
)
|
||||
|
||||
async def reload(self):
|
||||
self.instances = [
|
||||
await setting.get(self.bot.appname)
|
||||
for setting in self.setting_classes
|
||||
]
|
||||
181
src/modules/skins/skinlib.py
Normal file
181
src/modules/skins/skinlib.py
Normal file
@@ -0,0 +1,181 @@
|
||||
from collections import defaultdict
|
||||
from typing import Optional
|
||||
|
||||
from frozendict import frozendict
|
||||
import discord
|
||||
from discord.components import SelectOption
|
||||
from discord.app_commands import Choice
|
||||
|
||||
from gui.base import AppSkin
|
||||
from meta import LionBot
|
||||
from meta.logger import log_wrap
|
||||
|
||||
from .data import CustomSkinData
|
||||
|
||||
|
||||
def appskin_as_option(skin: AppSkin) -> SelectOption:
|
||||
"""
|
||||
Create a SelectOption from the given localised AppSkin
|
||||
"""
|
||||
return SelectOption(
|
||||
label=skin.display_name,
|
||||
description=skin.description,
|
||||
value=skin.skin_id,
|
||||
)
|
||||
|
||||
|
||||
def appskin_as_choice(skin: AppSkin) -> Choice[str]:
|
||||
"""
|
||||
Create an appcmds.Choice from the given localised AppSkin
|
||||
"""
|
||||
return Choice(
|
||||
name=skin.display_name,
|
||||
value=skin.skin_id,
|
||||
)
|
||||
|
||||
|
||||
class FrozenCustomSkin:
|
||||
__slots__ = ('base_skin_name', 'properties')
|
||||
|
||||
def __init__(self, base_skin_name: Optional[str], properties: dict[str, dict[str, str]]):
|
||||
self.base_skin_name = base_skin_name
|
||||
self.properties = frozendict((card, frozendict(props)) for card, props in properties.items())
|
||||
|
||||
def args_for(self, card_id: str):
|
||||
args = {}
|
||||
if self.base_skin_name is not None:
|
||||
args["base_skin_id"] = self.base_skin_name
|
||||
if card_id in self.properties:
|
||||
args.update(self.properties[card_id])
|
||||
return args
|
||||
|
||||
|
||||
class CustomSkin:
|
||||
def __init__(self,
|
||||
bot: LionBot,
|
||||
base_skin_name: Optional[str]=None,
|
||||
properties: dict[str, dict[str, str]] = {},
|
||||
data: Optional[CustomSkinData.CustomisedSkin]=None,
|
||||
):
|
||||
self.bot = bot
|
||||
self.data = data
|
||||
|
||||
self.base_skin_name = base_skin_name
|
||||
self.properties = properties
|
||||
|
||||
@property
|
||||
def cog(self):
|
||||
return self.bot.get_cog('CustomSkinCog')
|
||||
|
||||
@property
|
||||
def skinid(self) -> Optional[int]:
|
||||
return self.data.custom_skin_id if self.data else None
|
||||
|
||||
@property
|
||||
def base_skin_id(self) -> Optional[int]:
|
||||
if self.base_skin_name is not None:
|
||||
return self.cog.appskin_names.inverse[self.base_skin_name]
|
||||
|
||||
@classmethod
|
||||
async def fetch(cls, bot: LionBot, skinid: int) -> Optional['CustomSkin']:
|
||||
"""
|
||||
Fetch the specified skin from data.
|
||||
"""
|
||||
cog = bot.get_cog('CustomSkinCog')
|
||||
row = await cog.data.CustomisedSkin.fetch(skinid)
|
||||
if row is not None:
|
||||
records = await cog.data.custom_skin_info.select_where(
|
||||
custom_skin_id=skinid
|
||||
)
|
||||
properties = defaultdict(dict)
|
||||
for record in records:
|
||||
card_id = record['card_id']
|
||||
prop_name = record['property_name']
|
||||
prop_value = record['value']
|
||||
properties[card_id][prop_name] = prop_value
|
||||
if row.base_skin_id is not None:
|
||||
base_skin_name = cog.appskin_names[row.base_skin_id]
|
||||
else:
|
||||
base_skin_name = None
|
||||
self = cls(bot, base_skin_name, properties, data=row)
|
||||
return self
|
||||
|
||||
@log_wrap(action='Save Skin')
|
||||
async def save(self):
|
||||
if self.data is None:
|
||||
raise ValueError("Cannot save a dataless CustomSkin")
|
||||
|
||||
async with self.bot.db.connection() as conn:
|
||||
self.bot.db.conn = conn
|
||||
async with conn.transaction():
|
||||
skinid = self.skinid
|
||||
await self.data.update(base_skin_id=self.base_skin_id)
|
||||
await self.cog.data.skin_properties.delete_where(custom_skin_id=skinid)
|
||||
|
||||
props = {
|
||||
(card, name): value
|
||||
for card, card_props in self.properties.items()
|
||||
for name, value in card_props.items()
|
||||
if value is not None
|
||||
}
|
||||
# Ensure the properties exist in cache
|
||||
await self.cog.fetch_property_ids(*props.keys())
|
||||
|
||||
# Now bulk insert
|
||||
if props:
|
||||
await self.cog.data.skin_properties.insert_many(
|
||||
('custom_skin_id', 'property_id', 'value'),
|
||||
*(
|
||||
(skinid, self.cog.skin_properties.inverse[propkey], value)
|
||||
for propkey, value in props.items()
|
||||
)
|
||||
)
|
||||
await self.bot.global_dispatch('skin_updated', skinid)
|
||||
|
||||
def get_prop(self, card_id: str, prop_name: str) -> Optional[str]:
|
||||
return self.properties.get(card_id, {}).get(prop_name, None)
|
||||
|
||||
def set_prop(self, card_id: str, prop_name: str, value: Optional[str]):
|
||||
cardprops = self.properties.get(card_id, None)
|
||||
if value is None:
|
||||
if cardprops is not None:
|
||||
cardprops.pop(prop_name, None)
|
||||
else:
|
||||
if cardprops is None:
|
||||
cardprops = self.properties[card_id] = {}
|
||||
cardprops[prop_name] = value
|
||||
|
||||
def resolve_propid(self, propid: int) -> tuple[str, str]:
|
||||
return self.cog.skin_properties[propid]
|
||||
|
||||
def __getitem__(self, propid: int) -> Optional[str]:
|
||||
return self.get_prop(*self.resolve_propid(propid))
|
||||
|
||||
def __setitem__(self, propid: int, value: Optional[str]):
|
||||
return self.set_prop(*self.resolve_propid(propid), value)
|
||||
|
||||
def __delitem__(self, propid: int):
|
||||
card, name = self.resolve_propid(propid)
|
||||
self.properties.get(card, {}).pop(name, None)
|
||||
|
||||
def freeze(self) -> FrozenCustomSkin:
|
||||
"""
|
||||
Freeze the custom skin data into a memory efficient FrozenCustomSkin.
|
||||
"""
|
||||
return FrozenCustomSkin(self.base_skin_name, self.properties)
|
||||
|
||||
def load_frozen(self, frozen: FrozenCustomSkin):
|
||||
"""
|
||||
Update state from the given frozen state.
|
||||
"""
|
||||
self.base_skin_name = frozen.base_skin_name
|
||||
self.properties = dict((card, dict(props)) for card, props in frozen.properties.items())
|
||||
return self
|
||||
|
||||
def args_for(self, card_id: str):
|
||||
args = {}
|
||||
if self.base_skin_name is not None:
|
||||
args["base_skin_id"] = self.base_skin_name
|
||||
if card_id in self.properties:
|
||||
args.update(self.properties[card_id])
|
||||
return args
|
||||
552
src/modules/skins/userskinui.py
Normal file
552
src/modules/skins/userskinui.py
Normal file
@@ -0,0 +1,552 @@
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
|
||||
import discord
|
||||
from discord.ui.button import button, Button, ButtonStyle
|
||||
from discord.ui.select import select, Select, SelectOption
|
||||
from gui.base.AppSkin import AppSkin
|
||||
from gui.base.Card import Card
|
||||
|
||||
from meta import LionBot, conf
|
||||
from meta.errors import ResponseTimedOut, UserInputError
|
||||
from meta.logger import log_wrap
|
||||
from modules.premium.data import GemTransactionType
|
||||
from modules.premium.errors import BalanceTooLow
|
||||
from modules.skins.skinlib import CustomSkin, appskin_as_option
|
||||
from utils.ui import MessageUI, input, Confirm
|
||||
from utils.lib import MessageArgs, utc_now
|
||||
from gui import cards
|
||||
|
||||
from . import babel, logger
|
||||
from .data import CustomSkinData as Data
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class UserSkinUI(MessageUI):
|
||||
card_classes = [
|
||||
cards.ProfileCard,
|
||||
cards.StatsCard,
|
||||
cards.WeeklyGoalCard,
|
||||
cards.WeeklyStatsCard,
|
||||
cards.MonthlyGoalCard,
|
||||
cards.MonthlyStatsCard,
|
||||
]
|
||||
|
||||
def __init__(self, bot: LionBot, userid: int, callerid: int, **kwargs):
|
||||
super().__init__(callerid=callerid, **kwargs)
|
||||
|
||||
self.bot = bot
|
||||
self.cog = bot.get_cog('CustomSkinCog')
|
||||
self.gems = bot.get_cog('PremiumCog')
|
||||
|
||||
self.userid = userid
|
||||
|
||||
# UI State
|
||||
# Map of app_skin_id -> itemid
|
||||
self.inventory: dict[str, int] = {}
|
||||
|
||||
# Active app skin, if any
|
||||
self.active: Optional[str] = None
|
||||
|
||||
# Skins available for purchase
|
||||
self.available = self._get_available()
|
||||
|
||||
# Index of card currently showing
|
||||
self._card: int = 0
|
||||
|
||||
# Name of skin currently displayed, 'default' for default
|
||||
self._skin: Optional[str] = None
|
||||
|
||||
self.balance: int = 0
|
||||
|
||||
@property
|
||||
def current_card(self) -> Card:
|
||||
return self.card_classes[self._card]
|
||||
|
||||
@property
|
||||
def current_skin(self) -> AppSkin:
|
||||
if self._skin is None:
|
||||
raise ValueError("Cannot get current skin before load.")
|
||||
return self.available[self._skin]
|
||||
|
||||
@property
|
||||
def is_default(self) -> bool:
|
||||
return (self._skin == 'default')
|
||||
|
||||
@property
|
||||
def is_owned(self) -> bool:
|
||||
return self.is_default or (self._skin in self.inventory)
|
||||
|
||||
@property
|
||||
def is_equipped(self) -> bool:
|
||||
return (self.active == self._skin) or (self.is_default and not self.active)
|
||||
|
||||
def _get_available(self) -> dict[str, AppSkin]:
|
||||
skins = {
|
||||
skin.skin_id: skin for skin in AppSkin.get_all()
|
||||
if skin.public or (
|
||||
skin.user_whitelist is not None and
|
||||
self.userid in skin.user_whitelist
|
||||
)
|
||||
}
|
||||
skins['default'] = self._make_default()
|
||||
return skins
|
||||
|
||||
def _make_default(self) -> AppSkin:
|
||||
"""
|
||||
Create a placeholder 'default' skin.
|
||||
"""
|
||||
t = self.bot.translator.t
|
||||
|
||||
skin = AppSkin(None)
|
||||
skin.skin_id = 'default'
|
||||
skin.display_name = t(_p(
|
||||
'ui:userskins|default_skin:display_name',
|
||||
"Default"
|
||||
))
|
||||
skin.description = t(_p(
|
||||
'ui:userskins|default_skin:description',
|
||||
"My default interface theme"
|
||||
))
|
||||
skin.price = 0
|
||||
return skin
|
||||
|
||||
# ----- UI API -----
|
||||
|
||||
@log_wrap(action='equip skin')
|
||||
async def _equip_owned_skin(self, itemid: Optional[int]):
|
||||
"""
|
||||
Equip the provided item.
|
||||
|
||||
if `itemid` is None, 'equips' the default skin.
|
||||
"""
|
||||
# Global dispatch
|
||||
await self.cog.data.UserSkin.table.update_where(
|
||||
userid=self.userid
|
||||
).set(active=False)
|
||||
if itemid is not None:
|
||||
await self.cog.data.UserSkin.table.update_where(
|
||||
userid=self.userid, itemid=itemid
|
||||
).set(active=True)
|
||||
|
||||
await self.bot.global_dispatch('userset_skin', self.userid)
|
||||
|
||||
@log_wrap(action='purchase skin')
|
||||
async def _purchase_skin(self, app_skin_name: str):
|
||||
async with self.bot.db.connection() as conn:
|
||||
self.bot.db.conn = conn
|
||||
async with conn.transaction():
|
||||
skin = self.current_skin
|
||||
skinid = self.cog.appskin_names.inverse[skin.skin_id]
|
||||
|
||||
# Perform transaction
|
||||
transaction = await self.gems.gem_transaction(
|
||||
GemTransactionType.PURCHASE,
|
||||
actorid=self.userid,
|
||||
from_account=self.userid,
|
||||
to_account=None,
|
||||
amount=skin.price,
|
||||
description=(
|
||||
f"User purchased custom app skin {skin.skin_id} via UserSkinUI."
|
||||
),
|
||||
note=None,
|
||||
reference=f"iid: {self._original.id if self._original else 'None'}"
|
||||
)
|
||||
|
||||
# Create custom skin
|
||||
custom_skin = await self.cog.data.CustomisedSkin.create(
|
||||
base_skin_id=skinid,
|
||||
)
|
||||
|
||||
# Update inventory actives
|
||||
await self.cog.data.UserSkin.table.update_where(
|
||||
userid=self.userid
|
||||
).set(active=False)
|
||||
|
||||
# Insert into inventory
|
||||
await self.cog.data.UserSkin.create(
|
||||
userid=self.userid,
|
||||
custom_skin_id=custom_skin.custom_skin_id,
|
||||
transactionid=transaction.transactionid,
|
||||
active=True
|
||||
)
|
||||
|
||||
# Global dispatch update
|
||||
await self.bot.global_dispatch('userset_skin', self.userid)
|
||||
|
||||
logger.info(
|
||||
f"<uid: {self.userid}> purchased skin {skin.skin_id}."
|
||||
)
|
||||
|
||||
# ----- UI Components -----
|
||||
|
||||
# Gift Button
|
||||
@button(
|
||||
label="GIFT_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.green,
|
||||
)
|
||||
async def gift_button(self, press: discord.Interaction, pressed: Button):
|
||||
# TODO: Replace with an actual gifting interface
|
||||
|
||||
t = self.bot.translator.t
|
||||
skin = self.current_skin
|
||||
gift_hint = t(_p(
|
||||
'ui:userskins|button:gift|response',
|
||||
"To gift **{skin}** to a friend,"
|
||||
" send them {gem}**{price}** with {gift_cmd}."
|
||||
)).format(
|
||||
skin=skin.display_name,
|
||||
gem=self.bot.config.emojis.gem,
|
||||
price=skin.price,
|
||||
gift_cmd=self.bot.core.mention_cmd('gift'),
|
||||
)
|
||||
await press.response.send_message(gift_hint, ephemeral=True)
|
||||
|
||||
async def gift_button_refresh(self):
|
||||
button = self.gift_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:userskins|button:gift|label',
|
||||
"Gift to a friend"
|
||||
))
|
||||
price = self.current_skin.price
|
||||
button.disabled = (
|
||||
not price or (price > self.balance)
|
||||
)
|
||||
|
||||
# Purchase Button
|
||||
@button(
|
||||
label="PURCHASE_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.green
|
||||
)
|
||||
async def purchase_button(self, press: discord.Interaction, pressed: Button):
|
||||
t = self.bot.translator.t
|
||||
|
||||
skin = self.current_skin
|
||||
|
||||
# Verify we can purchase this skin
|
||||
await self.reload()
|
||||
|
||||
if self.is_owned:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'ui:userskins|button:purchase|error:already_owned',
|
||||
"You already own this skin!"
|
||||
))
|
||||
)
|
||||
elif skin.price > self.balance:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'ui:userskins|button:purchase|error:insufficient_gems',
|
||||
"You don't have enough LionGems to purchase this skin!"
|
||||
))
|
||||
)
|
||||
|
||||
# Confirm purchase
|
||||
confirm_msg = t(_p(
|
||||
'ui:userskins|button:purchase|confirm|desc',
|
||||
"Are you sure you want to purchase this skin?\n"
|
||||
"The price of the skin is {gem}**{price}**."
|
||||
)).format(price=skin.price, gem=self.bot.config.emojis.gem)
|
||||
confirm = Confirm(confirm_msg, press.user.id)
|
||||
|
||||
confirm.embed.set_footer(
|
||||
text=t(_p(
|
||||
'ui:userskins|button:purchase|confirm|footer',
|
||||
"Your current balance is {balance} LionGems"
|
||||
)).format(balance=self.balance)
|
||||
)
|
||||
|
||||
try:
|
||||
result = await confirm.ask(press, ephemeral=True)
|
||||
except ResponseTimedOut:
|
||||
result = False
|
||||
|
||||
if result:
|
||||
try:
|
||||
await self._purchase_skin(skin.skin_id)
|
||||
except BalanceTooLow:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'ui:userskins|button:purchase|error:insufficient_gems_post_confirm',
|
||||
"Insufficient LionGems to purchase this skin!"
|
||||
))
|
||||
)
|
||||
|
||||
# Ack purchase and refresh
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.brand_green(),
|
||||
title=t(_p(
|
||||
'ui:userskins|button:purchase|embed:success|title',
|
||||
"Skin Purchase"
|
||||
)),
|
||||
description=t(_p(
|
||||
'ui:userskins|button:purchase|embed:success|desc',
|
||||
"You have purchased and equipped the skin **{name}**!\n"
|
||||
"Thank you for your support, and enjoy your new purchase!"
|
||||
)).format(name=skin.display_name)
|
||||
)
|
||||
await press.followup.send(embed=embed, ephemeral=True)
|
||||
await self.refresh()
|
||||
|
||||
async def purchase_button_refresh(self):
|
||||
button = self.purchase_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:userskins|button:purchase|label',
|
||||
"Purchase Skin"
|
||||
))
|
||||
button.disabled = (
|
||||
self.is_owned
|
||||
or self.current_skin.price > self.balance
|
||||
)
|
||||
|
||||
# Equip Button
|
||||
@button(
|
||||
label="EQUIP_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.green
|
||||
)
|
||||
async def equip_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
t = self.bot.translator.t
|
||||
|
||||
to_equip = None if self.is_default else self.inventory[self._skin]
|
||||
await self._equip_owned_skin(to_equip)
|
||||
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.brand_green(),
|
||||
title=t(_p(
|
||||
'ui:userskins|button:equip|embed:success|title',
|
||||
"Skin Equipped"
|
||||
)),
|
||||
description=t(_p(
|
||||
'ui:userskins|button:equip|embed:success|desc',
|
||||
"You have equpped your **{name}** skin!"
|
||||
)).format(name=self.current_skin.display_name)
|
||||
)
|
||||
await press.edit_original_response(embed=embed)
|
||||
await self.refresh()
|
||||
|
||||
async def equip_button_refresh(self):
|
||||
button = self.equip_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:userskins|button:equip|label',
|
||||
"Equip Skin"
|
||||
))
|
||||
button.disabled = (
|
||||
self.is_equipped or not self.is_owned
|
||||
)
|
||||
|
||||
# Price button
|
||||
@button(
|
||||
label="PRICE_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.green,
|
||||
emoji=conf.emojis.gem,
|
||||
)
|
||||
async def price_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=False)
|
||||
|
||||
async def price_button_refresh(self):
|
||||
button = self.price_button
|
||||
t = self.bot.translator.t
|
||||
|
||||
price = self.current_skin.price
|
||||
button.label = t(_p(
|
||||
'ui:userskins|button:price|label',
|
||||
"{price} Gems"
|
||||
)).format(price=price)
|
||||
if price < self.balance:
|
||||
button.style = ButtonStyle.green
|
||||
else:
|
||||
button.style = ButtonStyle.danger
|
||||
|
||||
# Card Menu
|
||||
@select(
|
||||
cls=Select,
|
||||
placeholder="CARD_MENU_PLACEHOLDER",
|
||||
min_values=1, max_values=1
|
||||
)
|
||||
async def card_menu(self, selection: discord.Interaction, selected: Select):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
self._card = int(selected.values[0])
|
||||
await self.refresh(thinking=selection)
|
||||
|
||||
async def card_menu_refresh(self):
|
||||
menu = self.card_menu
|
||||
t = self.bot.translator.t
|
||||
menu.placeholder = t(_p(
|
||||
'ui:userskins|menu:card|placeholder',
|
||||
"Select a card to preview"
|
||||
))
|
||||
options = []
|
||||
for i, card in enumerate(self.card_classes):
|
||||
option = SelectOption(
|
||||
label=t(card.display_name),
|
||||
value=str(i),
|
||||
default=(i == self._card)
|
||||
)
|
||||
options.append(option)
|
||||
menu.options = options
|
||||
|
||||
@button(emoji=conf.emojis.cancel, style=ButtonStyle.red)
|
||||
async def quit_button(self, press: discord.Interaction, pressed: Button):
|
||||
"""
|
||||
Quit the UI.
|
||||
"""
|
||||
await press.response.defer()
|
||||
await self.quit()
|
||||
|
||||
# Skin Menu
|
||||
@select(
|
||||
cls=Select,
|
||||
placeholder="SKIN_MENU_PLACEHOLDER",
|
||||
min_values=1, max_values=1
|
||||
)
|
||||
async def skin_menu(self, selection: discord.Interaction, selected: Select):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
self._skin = selected.values[0]
|
||||
await self.refresh(thinking=selection)
|
||||
|
||||
async def skin_menu_refresh(self):
|
||||
menu = self.skin_menu
|
||||
t = self.bot.translator.t
|
||||
menu.placeholder = t(_p(
|
||||
'ui:userskins|menu:skin|placeholder',
|
||||
"Select a skin."
|
||||
))
|
||||
options = []
|
||||
for skin in self.available.values():
|
||||
option = appskin_as_option(skin)
|
||||
if skin.skin_id == self._skin:
|
||||
option.default = True
|
||||
options.append(option)
|
||||
menu.options = options
|
||||
|
||||
# ----- UI Flow -----
|
||||
async def _render_card(self) -> discord.File:
|
||||
if not self._skin:
|
||||
raise ValueError("Rendering UserSkinUI before load.")
|
||||
|
||||
use_skin = None
|
||||
if self._skin == 'default':
|
||||
use_skin = await self.cog.get_default_skin()
|
||||
else:
|
||||
use_skin = self._skin
|
||||
skin = {'base_skin_id': use_skin} if use_skin else {}
|
||||
|
||||
return await self.current_card.generate_sample(skin=skin)
|
||||
|
||||
async def make_message(self) -> MessageArgs:
|
||||
if not self._skin:
|
||||
raise ValueError("Rendering UserSkinUI before load.")
|
||||
|
||||
t = self.bot.translator.t
|
||||
|
||||
skin = self.current_skin
|
||||
|
||||
# Compute tagline
|
||||
if not self.is_owned:
|
||||
if skin.price <= self.balance:
|
||||
tagline = t(_p(
|
||||
'ui:userskins|tagline:purchase',
|
||||
"Purchase this skin for {gem}{price}"
|
||||
))
|
||||
else:
|
||||
tagline = t(_p(
|
||||
'ui:userskins|tagline:insufficient',
|
||||
"You don't have enough LionGems to buy this skin!"
|
||||
))
|
||||
elif not self.is_equipped:
|
||||
tagline = t(_p(
|
||||
'ui:userskins|tagline:equip',
|
||||
"You already own this skin! Clock Equip to use it!"
|
||||
))
|
||||
else:
|
||||
tagline = t(_p(
|
||||
'ui:userskins|tagline:current',
|
||||
"This is your current skin!"
|
||||
))
|
||||
|
||||
tagline = tagline.format(
|
||||
gem=self.bot.config.emojis.gem,
|
||||
price=skin.price,
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.orange(),
|
||||
title=skin.display_name,
|
||||
description=f"{skin.description}\n\n***{tagline}***"
|
||||
)
|
||||
embed.set_footer(
|
||||
icon_url="https://cdn.discordapp.com/attachments/925799205954543636/938703943683416074/4CF1C849-D532-4DEC-B4C9-0AB11F443BAB.png",
|
||||
text=t(_p(
|
||||
'ui:userskins|footer',
|
||||
"Current Balance: {balance} LionGems"
|
||||
)).format(balance=self.balance)
|
||||
)
|
||||
embed.set_image(url='attachment://sample.png')
|
||||
|
||||
file = await self._render_card()
|
||||
|
||||
return MessageArgs(embed=embed, files=[file])
|
||||
|
||||
async def refresh_layout(self):
|
||||
"""
|
||||
(gift_button, price_button, action_button)
|
||||
(skin_menu,),
|
||||
(card_menu,),
|
||||
"""
|
||||
to_refresh = (
|
||||
self.gift_button_refresh(),
|
||||
self.price_button_refresh(),
|
||||
self.purchase_button_refresh(),
|
||||
self.equip_button_refresh(),
|
||||
self.card_menu_refresh(),
|
||||
self.skin_menu_refresh(),
|
||||
)
|
||||
await asyncio.gather(*to_refresh)
|
||||
|
||||
# Determine action button
|
||||
skin = self.current_skin
|
||||
if not self.is_owned:
|
||||
if skin.price <= self.balance:
|
||||
action = self.purchase_button
|
||||
else:
|
||||
action = self.gems.buy_gems_button()
|
||||
else:
|
||||
action = self.equip_button
|
||||
|
||||
self.set_layout(
|
||||
(self.gift_button, self.price_button, action, self.quit_button,),
|
||||
(self.skin_menu,),
|
||||
(self.card_menu,),
|
||||
)
|
||||
|
||||
async def reload(self):
|
||||
"""
|
||||
Load the user's skin inventory.
|
||||
"""
|
||||
records = await self.cog.data.UserSkin.table.select_where(
|
||||
userid=self.userid
|
||||
).join(
|
||||
'customised_skins', using=('custom_skin_id',)
|
||||
).select(
|
||||
'itemid', 'custom_skin_id', 'base_skin_id', 'active'
|
||||
).with_no_adapter()
|
||||
active = None
|
||||
inventory = {}
|
||||
for record in records:
|
||||
base_skin_name = self.cog.appskin_names[record['base_skin_id']]
|
||||
inventory[base_skin_name] = record['itemid']
|
||||
if record['active']:
|
||||
active = base_skin_name
|
||||
|
||||
self.inventory = inventory
|
||||
self.active = active
|
||||
if self._skin is None:
|
||||
self._skin = active or 'default'
|
||||
|
||||
self.balance = await self.gems.get_gem_balance(self.userid)
|
||||
@@ -36,9 +36,13 @@ class SponsorCog(LionCog):
|
||||
"""
|
||||
if not interaction.is_expired():
|
||||
# TODO: caching
|
||||
whitelist = (await self.settings.Whitelist.get(self.bot.appname)).value
|
||||
if interaction.guild and interaction.guild.id in whitelist:
|
||||
return
|
||||
if interaction.guild:
|
||||
whitelist = (await self.settings.Whitelist.get(self.bot.appname)).value
|
||||
if interaction.guild.id in whitelist:
|
||||
return
|
||||
premiumcog = self.bot.get_cog('PremiumCog')
|
||||
if premiumcog and await premiumcog.is_premium_guild(interaction.guild.id):
|
||||
return
|
||||
setting = await self.settings.SponsorPrompt.get(self.bot.appname)
|
||||
value = setting.value
|
||||
if value:
|
||||
|
||||
@@ -122,6 +122,11 @@ async def get_goals_card(
|
||||
badges = await data.ProfileTag.fetch_tags(guildid, userid)
|
||||
|
||||
card_cls = WeeklyGoalCard if weekly else MonthlyGoalCard
|
||||
|
||||
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||
guildid, userid, card_cls.card_id
|
||||
)
|
||||
|
||||
card = card_cls(
|
||||
name=username[0],
|
||||
discrim=username[1],
|
||||
@@ -134,6 +139,6 @@ async def get_goals_card(
|
||||
attendance=attendance,
|
||||
goals=tasks,
|
||||
date=today,
|
||||
skin={'mode': mode}
|
||||
skin=skin | {'mode': mode}
|
||||
)
|
||||
return card
|
||||
|
||||
@@ -60,8 +60,12 @@ async def get_leaderboard_card(
|
||||
highlight = position
|
||||
|
||||
# Request Card
|
||||
|
||||
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||
guildid, None, LeaderboardCard.card_id
|
||||
)
|
||||
card = LeaderboardCard(
|
||||
skin={'mode': mode},
|
||||
skin=skin | {'mode': mode},
|
||||
server_name=guild.name,
|
||||
entries=entries,
|
||||
highlight=highlight
|
||||
|
||||
@@ -121,6 +121,9 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
username = (lion.data.display_name, '#????')
|
||||
|
||||
# Request card
|
||||
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||
guildid, userid, MonthlyStatsCard.card_id
|
||||
)
|
||||
card = MonthlyStatsCard(
|
||||
user=username,
|
||||
timezone=str(lion.timezone),
|
||||
@@ -129,6 +132,6 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
monthly=monthly,
|
||||
current_streak=current_streak,
|
||||
longest_streak=longest_streak,
|
||||
skin={'mode': mode}
|
||||
skin=skin | {'mode': mode}
|
||||
)
|
||||
return card
|
||||
|
||||
@@ -80,6 +80,10 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int):
|
||||
achievements = await get_achievements_for(bot, guildid, userid)
|
||||
achieved = tuple(ach.emoji_index for ach in achievements if ach.achieved)
|
||||
|
||||
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||
guildid, userid, ProfileCard.card_id
|
||||
)
|
||||
|
||||
card = ProfileCard(
|
||||
user=username,
|
||||
avatar=(userid, avatar),
|
||||
@@ -88,6 +92,7 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int):
|
||||
achievements=achieved,
|
||||
current_rank=current_rank,
|
||||
rank_progress=rank_progress,
|
||||
next_rank=next_rank
|
||||
next_rank=next_rank,
|
||||
skin=skin,
|
||||
)
|
||||
return card
|
||||
|
||||
@@ -117,12 +117,16 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int, mode: CardMode
|
||||
if streak_start is not None:
|
||||
streaks.append((streak_start, today.day))
|
||||
|
||||
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||
guildid, userid, StatsCard.card_id
|
||||
)
|
||||
|
||||
card = StatsCard(
|
||||
(position, 0),
|
||||
period_strings,
|
||||
month_string,
|
||||
100,
|
||||
streaks,
|
||||
skin={'mode': mode}
|
||||
skin=skin | {'mode': mode}
|
||||
)
|
||||
return card
|
||||
|
||||
@@ -58,6 +58,10 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
else:
|
||||
username = (lion.data.display_name, '#????')
|
||||
|
||||
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||
guildid, userid, WeeklyStatsCard.card_id
|
||||
)
|
||||
|
||||
card = WeeklyStatsCard(
|
||||
user=username,
|
||||
timezone=str(lion.timezone),
|
||||
@@ -68,6 +72,6 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
(int(session['start_time'].timestamp()), int(session['start_time'].timestamp() + int(session['duration'])))
|
||||
for session in sessions
|
||||
],
|
||||
skin={'mode': mode}
|
||||
skin=skin | {'mode': mode}
|
||||
)
|
||||
return card
|
||||
|
||||
@@ -537,10 +537,11 @@ class LeaderboardUI(StatsUI):
|
||||
period_row,
|
||||
page_row
|
||||
]
|
||||
|
||||
voting = self.bot.get_cog('TopggCog')
|
||||
if voting and not await voting.check_voted_recently(self.userid):
|
||||
self._layout.append((voting.vote_button(),))
|
||||
premiumcog = self.bot.get_cog('PremiumCog')
|
||||
if not (premiumcog and await premiumcog.is_premium_guild(self.guild.id)):
|
||||
self._layout.append((voting.vote_button(),))
|
||||
|
||||
async def reload(self):
|
||||
"""
|
||||
|
||||
@@ -299,7 +299,9 @@ class ProfileUI(StatsUI):
|
||||
|
||||
voting = self.bot.get_cog('TopggCog')
|
||||
if voting and not await voting.check_voted_recently(self.userid):
|
||||
self._layout.append((voting.vote_button(),))
|
||||
premiumcog = self.bot.get_cog('PremiumCog')
|
||||
if not (premiumcog and await premiumcog.is_premium_guild(self.guild.id)):
|
||||
self._layout.append((voting.vote_button(),))
|
||||
|
||||
async def _render_stats(self):
|
||||
"""
|
||||
|
||||
@@ -753,7 +753,9 @@ class WeeklyMonthlyUI(StatsUI):
|
||||
|
||||
voting = self.bot.get_cog('TopggCog')
|
||||
if voting and not await voting.check_voted_recently(self.userid):
|
||||
self._layout.append((voting.vote_button(),))
|
||||
premiumcog = self.bot.get_cog('PremiumCog')
|
||||
if not (premiumcog and await premiumcog.is_premium_guild(self.guild.id)):
|
||||
self._layout.append((voting.vote_button(),))
|
||||
|
||||
if self._showing_selector:
|
||||
await self.period_menu_refresh()
|
||||
|
||||
@@ -431,7 +431,7 @@ class Exec(LionCog):
|
||||
results = [
|
||||
appcmd.Choice(name=f"No extensions found matching {partial}", value="None")
|
||||
]
|
||||
return results
|
||||
return results[:25]
|
||||
|
||||
@commands.hybrid_command(
|
||||
name=_('shutdown'),
|
||||
|
||||
Reference in New Issue
Block a user