53
LICENSE.md
53
LICENSE.md
@@ -1,10 +1,45 @@
|
|||||||
Copyright (c) 2022, Ari Horesh.
|
StudyLion Open Source License
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
Copyright (c) 2023, Ari Horesh. All rights reserved.
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
### 1. Definitions
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
- "Software": Refers to the Discord bot named "StudyLion" and associated documentation, source code, scripts, assets, and other related materials.
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
- "Educational Use": Means utilization of the Software primarily for learning, teaching, training, research, or development.
|
||||||
SOFTWARE.
|
- "Non-commercial Use": Describes an application of the Software where there's no expectation or realization of direct or indirect monetary compensation.
|
||||||
|
|
||||||
|
### 2. Grant of License
|
||||||
|
|
||||||
|
- Under the terms and conditions of this license, the licensor grants a worldwide, non-exclusive, royalty-free, non-transferable license to:
|
||||||
|
- Use the Software.
|
||||||
|
- Reproduce the Software.
|
||||||
|
- Modify the Software, creating derivative works based on the Software.
|
||||||
|
- Distribute the unmodified Software for Educational and Non-commercial Use.
|
||||||
|
|
||||||
|
### 3. Restrictions
|
||||||
|
|
||||||
|
- Redistribution, whether modified or unmodified, must:
|
||||||
|
- Preserve the above copyright notice.
|
||||||
|
- Incorporate this list of conditions.
|
||||||
|
- Include the following disclaimers.
|
||||||
|
|
||||||
|
- You must not:
|
||||||
|
- Use the name, trademarks, service marks, or names of the Software or its contributors to endorse or promote derivative products without prior written consent.
|
||||||
|
- Deploy the Software or any derivative works thereof for commercial purposes or any context leading to financial gain.
|
||||||
|
- Assert proprietary rights or assign authorship of the original Software to any entity other than the original authors.
|
||||||
|
- Grant sublicenses for the Software.
|
||||||
|
|
||||||
|
### 4. Contributions
|
||||||
|
|
||||||
|
- Any contributions made to the Software by third parties shall be subject to this license. The contributor grants the licensor a non-exclusive, perpetual, irrevocable license to any such contributions.
|
||||||
|
|
||||||
|
### 5. Termination
|
||||||
|
|
||||||
|
- If you breach any terms of this license, your rights will terminate automatically. Once terminated, you must:
|
||||||
|
- Halt all utilization of the Software.
|
||||||
|
- Destroy or delete all copies of the Software in your possession or control.
|
||||||
|
|
||||||
|
### 6. Disclaimer and Limitation of Liability
|
||||||
|
|
||||||
|
- THE SOFTWARE IS OFFERED "AS IS", WITHOUT ANY GUARANTEES OR CLAIMS OF EFFICACY. NO WARRANTIES, IMPLIED OR EXPLICIT, ARE PROVIDED. THIS INCLUDES, BUT IS NOT RESTRICTED TO, WARRANTIES OF MERCHANDISE, FITNESS FOR A SPECIFIC PURPOSE, AND NON-INFRINGEMENT.
|
||||||
|
- UNDER NO CONDITION SHALL THE AUTHORS, COPYRIGHT HOLDERS, OR CONTRIBUTORS BE ACCOUNTABLE FOR ANY CLAIMS, DAMAGES, OR OTHER LIABILITIES, WHETHER RESULTING FROM CONTRACT, TORT, NEGLIGENCE, OR ANY OTHER LEGAL THEORY, EMERGING FROM, OUT OF, OR CONNECTED WITH THE SOFTWARE OR ITS USE.
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -1,13 +1,13 @@
|
|||||||
|
|
||||||
## StudyLion - Discord Study & Productivity Bot
|
## LionBot (formerly StudyLion) - Discord Study & Productivity Bot
|
||||||
|
|
||||||
StudyLion is a Discord bot that tracks members' study and work time while offering members the ability to view their statistics and use productivity tools such as: To-do lists, pomodoro timers, reminders, and much more.
|
LionBot is a Discord bot that tracks members' study and work time while offering members the ability to view their statistics and use productivity tools such as: To-do lists, pomodoro timers, reminders, and much more.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[**Invite StudyLion here**](https://discord.studylions.com/invite "here"), and get started with `!help`.
|
[**Invite LionBot here**](https://discord.com/oauth2/authorize?client_id=889078613817831495&permissions=8&scope=bot), and get started with `/help`.
|
||||||
|
|
||||||
Join the [**support server**](https://discord.gg/studylions "support server") to contact us if you need help configuring the bot on your server, or start a [**discussion**](https://github.com/StudyLions/StudyLion/discussions "disscussion") to report issues and bugs.
|
Join the [**support server**](https://discord.gg/the-study-lions-780195610154237993) to contact us if you need help configuring the bot on your server, or start a [**discussion**](https://github.com/LionBots/LionBot/discussions "disscussion") to report issues and bugs.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -18,14 +18,11 @@ In the past couple of years, we noticed a new trend on Discord – instead of be
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
This bot was founder by [Ari Horesh](https://www.youtube.com/arihoresh) (Ari Horesh#0001) to support these forming study communities and allow students all over the world to study better.
|
This bot was founder by [Ari Horesh](https://www.youtube.com/arihoresh) (@AriHoresh) to support these forming study communities and allow students all over the world to study better.
|
||||||
|
|
||||||
### Self Hosting
|
### Self Hosting
|
||||||
|
|
||||||
We offer private instances based on availablity (a private bot for your community) to server owners who want their own branding (logo, color scheme, private and seperate database, better response-rate, and customizability to the text itself).
|
You can self-host and fork the bot using the following steps, but beware that we do not provide support for self-hosted instances. If you are interested in a privately managed instance (affordable paid service), contact Ari at contact@arihoresh.com
|
||||||
If you are intrested, contact the founder at contact@arihoresh.com .
|
|
||||||
|
|
||||||
You can self-host and fork the bot using the following steps, but beware that this version **does not include** our visual graphical user interface, which is only include in the custom private instances or our the public instance.
|
|
||||||
|
|
||||||
Follow the steps below to self-host the bot.
|
Follow the steps below to self-host the bot.
|
||||||
- Clone the repo recursively (which makes sure to include the cmdClient submodule, otherwise you need to initialise it separately)
|
- Clone the repo recursively (which makes sure to include the cmdClient submodule, otherwise you need to initialise it separately)
|
||||||
@@ -38,7 +35,6 @@ We do not offer support for self-hosted bots, the code is provided as is without
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
||||||
- **Students Cards and Statistics**
|
- **Students Cards and Statistics**
|
||||||
|
|
||||||
Allow users to create their own private student profile cards and set customs study field tags by using `!stats` and `!setprofile`
|
Allow users to create their own private student profile cards and set customs study field tags by using `!stats` and `!setprofile`
|
||||||
@@ -108,8 +104,8 @@ Punish cheaters, audit-log, welcome message, and so much more using our full-sca
|
|||||||
|
|
||||||
### Tutorials
|
### Tutorials
|
||||||
|
|
||||||
A command list and general documentation for StudyLion may be found using the `!help` command, and documentation for a specific command, e.g. `config`, may be found with `!help config`.
|
A command list and general documentation for LionBot may be found using the `!help` command, and documentation for a specific command, e.g. `config`, may be found with `!help config`.
|
||||||
|
|
||||||
Make sure to check the [full documentation](https://www.notion.so/izabellakis/StudyLion-Bot-Tutorials-f493268fcd12436c9674afef2e151707 "StudyLion Tutorial") to stay updated.
|
Make sure to check the [full documentation](https://www.notion.so/izabellakis/LionBot-Bot-Tutorials-f493268fcd12436c9674afef2e151707 "LionBot Tutorial") to stay updated.
|
||||||
|
|
||||||
<a href="https://imgur.com/ziPdJGw"><img src="https://i.imgur.com/ziPdJGws.png" title="source: imgur.com" /></a>
|
<a href="https://imgur.com/ziPdJGw"><img src="https://i.imgur.com/ziPdJGws.png" title="source: imgur.com" /></a>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ invite_bot =
|
|||||||
|
|
||||||
[ENDPOINTS]
|
[ENDPOINTS]
|
||||||
guild_log =
|
guild_log =
|
||||||
gem_transaction =
|
gem_log =
|
||||||
|
|
||||||
[LOGGING]
|
[LOGGING]
|
||||||
log_file = bot.log
|
log_file = bot.log
|
||||||
|
|||||||
@@ -1218,7 +1218,7 @@ create TABLE timers(
|
|||||||
inactivity_threshold INTEGER,
|
inactivity_threshold INTEGER,
|
||||||
channel_name TEXT,
|
channel_name TEXT,
|
||||||
pretty_name TEXT,
|
pretty_name TEXT,
|
||||||
owenrid BIGINT REFERENCES user_config,
|
ownerid BIGINT REFERENCES user_config,
|
||||||
manager_roleid BIGINT,
|
manager_roleid BIGINT,
|
||||||
last_messageid BIGINT,
|
last_messageid BIGINT,
|
||||||
voice_alerts BOOLEAN,
|
voice_alerts BOOLEAN,
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ topggpy
|
|||||||
psutil
|
psutil
|
||||||
pillow
|
pillow
|
||||||
python-dateutil
|
python-dateutil
|
||||||
|
bidict
|
||||||
|
frozendict
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from weakref import WeakValueDictionary
|
from weakref import WeakValueDictionary
|
||||||
@@ -19,6 +20,8 @@ from .lion_member import MemberConfig
|
|||||||
from .lion_user import UserConfig
|
from .lion_user import UserConfig
|
||||||
from .hooks import HookedChannel
|
from .hooks import HookedChannel
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class keydefaultdict(defaultdict):
|
class keydefaultdict(defaultdict):
|
||||||
def __missing__(self, key):
|
def __missing__(self, key):
|
||||||
@@ -71,7 +74,6 @@ class CoreCog(LionCog):
|
|||||||
self.bot.add_listener(self.shard_update_guilds, name='on_guild_join')
|
self.bot.add_listener(self.shard_update_guilds, name='on_guild_join')
|
||||||
self.bot.add_listener(self.shard_update_guilds, name='on_guild_remove')
|
self.bot.add_listener(self.shard_update_guilds, name='on_guild_remove')
|
||||||
|
|
||||||
self.bot.core = self
|
|
||||||
await self.bot.add_cog(self.lions)
|
await self.bot.add_cog(self.lions)
|
||||||
|
|
||||||
# Load the app command cache
|
# Load the app command cache
|
||||||
@@ -127,3 +129,7 @@ class CoreCog(LionCog):
|
|||||||
@log_wrap(action='Update shard guilds')
|
@log_wrap(action='Update shard guilds')
|
||||||
async def shard_update_guilds(self, guild):
|
async def shard_update_guilds(self, guild):
|
||||||
await self.shard_data.update(guild_count=len(self.bot.guilds))
|
await self.shard_data.update(guild_count=len(self.bot.guilds))
|
||||||
|
|
||||||
|
@LionCog.listener('on_ping')
|
||||||
|
async def handle_ping(self, *args, **kwargs):
|
||||||
|
logger.info(f"Received ping with args {args}, kwargs {kwargs}")
|
||||||
|
|||||||
2
src/gui
2
src/gui
Submodule src/gui updated: f2760218ef...c1bcb05c25
@@ -1,4 +1,4 @@
|
|||||||
from typing import List, Optional, TYPE_CHECKING
|
from typing import List, Literal, LiteralString, Optional, TYPE_CHECKING, overload
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from weakref import WeakValueDictionary
|
from weakref import WeakValueDictionary
|
||||||
@@ -13,7 +13,7 @@ from aiohttp import ClientSession
|
|||||||
from data import Database
|
from data import Database
|
||||||
from utils.lib import tabulate
|
from utils.lib import tabulate
|
||||||
from gui.errors import RenderingException
|
from gui.errors import RenderingException
|
||||||
from babel.translator import ctx_locale
|
from babel.translator import ctx_locale, LeoBabel
|
||||||
|
|
||||||
from .config import Conf
|
from .config import Conf
|
||||||
from .logger import logging_context, log_context, log_action_stack, log_wrap, set_logging_context
|
from .logger import logging_context, log_context, log_action_stack, log_wrap, set_logging_context
|
||||||
@@ -24,16 +24,39 @@ from .errors import HandledException, SafeCancellation
|
|||||||
from .monitor import SystemMonitor, ComponentMonitor, StatusLevel, ComponentStatus
|
from .monitor import SystemMonitor, ComponentMonitor, StatusLevel, ComponentStatus
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core import CoreCog
|
from core.cog import CoreCog
|
||||||
|
from core.config import ConfigCog
|
||||||
|
from tracking.voice.cog import VoiceTrackerCog
|
||||||
|
from tracking.text.cog import TextTrackerCog
|
||||||
|
from modules.config.cog import GuildConfigCog
|
||||||
|
from modules.economy.cog import Economy
|
||||||
|
from modules.member_admin.cog import MemberAdminCog
|
||||||
|
from modules.meta.cog import MetaCog
|
||||||
|
from modules.moderation.cog import ModerationCog
|
||||||
|
from modules.pomodoro.cog import TimerCog
|
||||||
|
from modules.premium.cog import PremiumCog
|
||||||
|
from modules.ranks.cog import RankCog
|
||||||
|
from modules.reminders.cog import Reminders
|
||||||
|
from modules.rooms.cog import RoomCog
|
||||||
|
from modules.schedule.cog import ScheduleCog
|
||||||
|
from modules.shop.cog import ShopCog
|
||||||
|
from modules.skins.cog import CustomSkinCog
|
||||||
|
from modules.sponsors.cog import SponsorCog
|
||||||
|
from modules.statistics.cog import StatsCog
|
||||||
|
from modules.sysadmin.dash import LeoSettings
|
||||||
|
from modules.tasklist.cog import TasklistCog
|
||||||
|
from modules.topgg.cog import TopggCog
|
||||||
|
from modules.user_config.cog import UserConfigCog
|
||||||
|
from modules.video_channels.cog import VideoCog
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LionBot(Bot):
|
class LionBot(Bot):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *args, appname: str, shardname: str, db: Database, config: Conf,
|
self, *args, appname: str, shardname: str, db: Database, config: Conf, translator: LeoBabel,
|
||||||
initial_extensions: List[str], web_client: ClientSession, app_ipc,
|
initial_extensions: List[str], web_client: ClientSession, app_ipc,
|
||||||
testing_guilds: List[int] = [], translator=None, **kwargs
|
testing_guilds: List[int] = [], **kwargs
|
||||||
):
|
):
|
||||||
kwargs.setdefault('tree_cls', LionTree)
|
kwargs.setdefault('tree_cls', LionTree)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -46,7 +69,6 @@ class LionBot(Bot):
|
|||||||
# self.appdata = appdata
|
# self.appdata = appdata
|
||||||
self.config = config
|
self.config = config
|
||||||
self.app_ipc = app_ipc
|
self.app_ipc = app_ipc
|
||||||
self.core: 'CoreCog' = None
|
|
||||||
self.translator = translator
|
self.translator = translator
|
||||||
|
|
||||||
self.system_monitor = SystemMonitor()
|
self.system_monitor = SystemMonitor()
|
||||||
@@ -56,6 +78,18 @@ class LionBot(Bot):
|
|||||||
self._locks = WeakValueDictionary()
|
self._locks = WeakValueDictionary()
|
||||||
self._running_events = set()
|
self._running_events = set()
|
||||||
|
|
||||||
|
self._talk_global_dispatch = app_ipc.register_route('dispatch')(self._handle_global_dispatch)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def core(self):
|
||||||
|
return self.get_cog('CoreCog')
|
||||||
|
|
||||||
|
async def _handle_global_dispatch(self, event_name: str, *args, **kwargs):
|
||||||
|
self.dispatch(event_name, *args, **kwargs)
|
||||||
|
|
||||||
|
async def global_dispatch(self, event_name: str, *args, **kwargs):
|
||||||
|
await self._talk_global_dispatch(event_name, *args, **kwargs).broadcast(except_self=False)
|
||||||
|
|
||||||
async def _monitor_status(self):
|
async def _monitor_status(self):
|
||||||
if self.is_closed():
|
if self.is_closed():
|
||||||
level = StatusLevel.ERRORED
|
level = StatusLevel.ERRORED
|
||||||
@@ -99,6 +133,112 @@ class LionBot(Bot):
|
|||||||
self.tree.copy_global_to(guild=guild)
|
self.tree.copy_global_to(guild=guild)
|
||||||
await self.tree.sync(guild=guild)
|
await self.tree.sync(guild=guild)
|
||||||
|
|
||||||
|
# To make the type checker happy about fetching cogs by name
|
||||||
|
# TODO: Move this to stubs at some point
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['CoreCog']) -> 'CoreCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['ConfigCog']) -> 'ConfigCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['VoiceTrackerCog']) -> 'VoiceTrackerCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['TextTrackerCog']) -> 'TextTrackerCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['GuildConfigCog']) -> 'GuildConfigCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['Economy']) -> 'Economy':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['MemberAdminCog']) -> 'MemberAdminCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['MetaCog']) -> 'MetaCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['ModerationCog']) -> 'ModerationCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['TimerCog']) -> 'TimerCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['PremiumCog']) -> 'PremiumCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['RankCog']) -> 'RankCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['Reminders']) -> 'Reminders':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['RoomCog']) -> 'RoomCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['ScheduleCog']) -> 'ScheduleCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['ShopCog']) -> 'ShopCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['CustomSkinCog']) -> 'CustomSkinCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['SponsorCog']) -> 'SponsorCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['StatsCog']) -> 'StatsCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['LeoSettings']) -> 'LeoSettings':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['TasklistCog']) -> 'TasklistCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['TopggCog']) -> 'TopggCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['UserConfigCog']) -> 'UserConfigCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: Literal['VideoCog']) -> 'VideoCog':
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_cog(self, name: str) -> Optional[Cog]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_cog(self, name: str) -> Optional[Cog]:
|
||||||
|
return super().get_cog(name)
|
||||||
|
|
||||||
async def add_cog(self, cog: Cog, **kwargs):
|
async def add_cog(self, cog: Cog, **kwargs):
|
||||||
sup = super()
|
sup = super()
|
||||||
@log_wrap(action=f"Attach {cog.__cog_name__}")
|
@log_wrap(action=f"Attach {cog.__cog_name__}")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ active = [
|
|||||||
'.sysadmin',
|
'.sysadmin',
|
||||||
'.config',
|
'.config',
|
||||||
'.user_config',
|
'.user_config',
|
||||||
|
'.skins',
|
||||||
'.schedule',
|
'.schedule',
|
||||||
'.economy',
|
'.economy',
|
||||||
'.ranks',
|
'.ranks',
|
||||||
@@ -20,6 +21,7 @@ active = [
|
|||||||
'.meta',
|
'.meta',
|
||||||
'.sponsors',
|
'.sponsors',
|
||||||
'.topgg',
|
'.topgg',
|
||||||
|
'.premium',
|
||||||
'.test',
|
'.test',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from typing import Optional
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
from core.lion_guild import LionGuild
|
from core.lion_guild import LionGuild
|
||||||
|
from data.queries import ORDER
|
||||||
from meta import LionBot
|
from meta import LionBot
|
||||||
from utils.lib import MessageArgs, jumpto, strfdelta, utc_now
|
from utils.lib import MessageArgs, jumpto, strfdelta, utc_now
|
||||||
from utils.monitor import TaskMonitor
|
from utils.monitor import TaskMonitor
|
||||||
@@ -86,7 +87,9 @@ class Ticket:
|
|||||||
instantiate the correct classes.
|
instantiate the correct classes.
|
||||||
"""
|
"""
|
||||||
registry: ModerationData = bot.db.registries['ModerationData']
|
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 = []
|
tickets = []
|
||||||
if rows:
|
if rows:
|
||||||
guildids = set(row.guildid for row in 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:
|
else:
|
||||||
card_cls = BreakTimerCard
|
card_cls = BreakTimerCard
|
||||||
|
|
||||||
|
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||||
|
timer.data.guildid, None, card_cls.card_id
|
||||||
|
)
|
||||||
|
|
||||||
return card_cls(
|
return card_cls(
|
||||||
name,
|
name,
|
||||||
remaining,
|
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
|
# Calculate destination
|
||||||
to_dm = lguild.config.get('dm_ranks').value
|
to_dm = lguild.config.get('dm_ranks').value
|
||||||
rank_channel = lguild.config.get('rank_channel').value
|
rank_channel = lguild.config.get('rank_channel').value
|
||||||
|
sent = False
|
||||||
|
|
||||||
if to_dm or not rank_channel:
|
if to_dm:
|
||||||
destination = member
|
destination = member
|
||||||
embed.set_author(
|
embed.set_author(
|
||||||
name=guild.name,
|
name=guild.name,
|
||||||
icon_url=guild.icon.url if guild.icon else None
|
icon_url=guild.icon.url if guild.icon else None
|
||||||
)
|
)
|
||||||
text = None
|
try:
|
||||||
else:
|
await destination.send(embed=embed)
|
||||||
|
sent = True
|
||||||
|
except discord.HTTPException:
|
||||||
|
if not rank_channel:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not sent and rank_channel:
|
||||||
destination = rank_channel
|
destination = rank_channel
|
||||||
text = member.mention
|
text = member.mention
|
||||||
|
await destination.send(content=text, embed=embed)
|
||||||
# Post!
|
|
||||||
await destination.send(embed=embed, content=text)
|
|
||||||
|
|
||||||
def get_message_map(self,
|
def get_message_map(self,
|
||||||
rank_type: RankType,
|
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,8 +36,12 @@ class SponsorCog(LionCog):
|
|||||||
"""
|
"""
|
||||||
if not interaction.is_expired():
|
if not interaction.is_expired():
|
||||||
# TODO: caching
|
# TODO: caching
|
||||||
|
if interaction.guild:
|
||||||
whitelist = (await self.settings.Whitelist.get(self.bot.appname)).value
|
whitelist = (await self.settings.Whitelist.get(self.bot.appname)).value
|
||||||
if interaction.guild and interaction.guild.id in whitelist:
|
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
|
return
|
||||||
setting = await self.settings.SponsorPrompt.get(self.bot.appname)
|
setting = await self.settings.SponsorPrompt.get(self.bot.appname)
|
||||||
value = setting.value
|
value = setting.value
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ async def get_goals_card(
|
|||||||
badges = await data.ProfileTag.fetch_tags(guildid, userid)
|
badges = await data.ProfileTag.fetch_tags(guildid, userid)
|
||||||
|
|
||||||
card_cls = WeeklyGoalCard if weekly else MonthlyGoalCard
|
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(
|
card = card_cls(
|
||||||
name=username[0],
|
name=username[0],
|
||||||
discrim=username[1],
|
discrim=username[1],
|
||||||
@@ -134,6 +139,6 @@ async def get_goals_card(
|
|||||||
attendance=attendance,
|
attendance=attendance,
|
||||||
goals=tasks,
|
goals=tasks,
|
||||||
date=today,
|
date=today,
|
||||||
skin={'mode': mode}
|
skin=skin | {'mode': mode}
|
||||||
)
|
)
|
||||||
return card
|
return card
|
||||||
|
|||||||
@@ -60,8 +60,12 @@ async def get_leaderboard_card(
|
|||||||
highlight = position
|
highlight = position
|
||||||
|
|
||||||
# Request Card
|
# Request Card
|
||||||
|
|
||||||
|
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||||
|
guildid, None, LeaderboardCard.card_id
|
||||||
|
)
|
||||||
card = LeaderboardCard(
|
card = LeaderboardCard(
|
||||||
skin={'mode': mode},
|
skin=skin | {'mode': mode},
|
||||||
server_name=guild.name,
|
server_name=guild.name,
|
||||||
entries=entries,
|
entries=entries,
|
||||||
highlight=highlight
|
highlight=highlight
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
|||||||
username = (lion.data.display_name, '#????')
|
username = (lion.data.display_name, '#????')
|
||||||
|
|
||||||
# Request card
|
# Request card
|
||||||
|
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||||
|
guildid, userid, MonthlyStatsCard.card_id
|
||||||
|
)
|
||||||
card = MonthlyStatsCard(
|
card = MonthlyStatsCard(
|
||||||
user=username,
|
user=username,
|
||||||
timezone=str(lion.timezone),
|
timezone=str(lion.timezone),
|
||||||
@@ -129,6 +132,6 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
|||||||
monthly=monthly,
|
monthly=monthly,
|
||||||
current_streak=current_streak,
|
current_streak=current_streak,
|
||||||
longest_streak=longest_streak,
|
longest_streak=longest_streak,
|
||||||
skin={'mode': mode}
|
skin=skin | {'mode': mode}
|
||||||
)
|
)
|
||||||
return card
|
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)
|
achievements = await get_achievements_for(bot, guildid, userid)
|
||||||
achieved = tuple(ach.emoji_index for ach in achievements if ach.achieved)
|
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(
|
card = ProfileCard(
|
||||||
user=username,
|
user=username,
|
||||||
avatar=(userid, avatar),
|
avatar=(userid, avatar),
|
||||||
@@ -88,6 +92,7 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int):
|
|||||||
achievements=achieved,
|
achievements=achieved,
|
||||||
current_rank=current_rank,
|
current_rank=current_rank,
|
||||||
rank_progress=rank_progress,
|
rank_progress=rank_progress,
|
||||||
next_rank=next_rank
|
next_rank=next_rank,
|
||||||
|
skin=skin,
|
||||||
)
|
)
|
||||||
return card
|
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:
|
if streak_start is not None:
|
||||||
streaks.append((streak_start, today.day))
|
streaks.append((streak_start, today.day))
|
||||||
|
|
||||||
|
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||||
|
guildid, userid, StatsCard.card_id
|
||||||
|
)
|
||||||
|
|
||||||
card = StatsCard(
|
card = StatsCard(
|
||||||
(position, 0),
|
(position, 0),
|
||||||
period_strings,
|
period_strings,
|
||||||
month_string,
|
month_string,
|
||||||
100,
|
100,
|
||||||
streaks,
|
streaks,
|
||||||
skin={'mode': mode}
|
skin=skin | {'mode': mode}
|
||||||
)
|
)
|
||||||
return card
|
return card
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
|||||||
else:
|
else:
|
||||||
username = (lion.data.display_name, '#????')
|
username = (lion.data.display_name, '#????')
|
||||||
|
|
||||||
|
skin = await bot.get_cog('CustomSkinCog').get_skinargs_for(
|
||||||
|
guildid, userid, WeeklyStatsCard.card_id
|
||||||
|
)
|
||||||
|
|
||||||
card = WeeklyStatsCard(
|
card = WeeklyStatsCard(
|
||||||
user=username,
|
user=username,
|
||||||
timezone=str(lion.timezone),
|
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'])))
|
(int(session['start_time'].timestamp()), int(session['start_time'].timestamp() + int(session['duration'])))
|
||||||
for session in sessions
|
for session in sessions
|
||||||
],
|
],
|
||||||
skin={'mode': mode}
|
skin=skin | {'mode': mode}
|
||||||
)
|
)
|
||||||
return card
|
return card
|
||||||
|
|||||||
@@ -537,9 +537,10 @@ class LeaderboardUI(StatsUI):
|
|||||||
period_row,
|
period_row,
|
||||||
page_row
|
page_row
|
||||||
]
|
]
|
||||||
|
|
||||||
voting = self.bot.get_cog('TopggCog')
|
voting = self.bot.get_cog('TopggCog')
|
||||||
if voting and not await voting.check_voted_recently(self.userid):
|
if voting and not await voting.check_voted_recently(self.userid):
|
||||||
|
premiumcog = self.bot.get_cog('PremiumCog')
|
||||||
|
if not (premiumcog and await premiumcog.is_premium_guild(self.guild.id)):
|
||||||
self._layout.append((voting.vote_button(),))
|
self._layout.append((voting.vote_button(),))
|
||||||
|
|
||||||
async def reload(self):
|
async def reload(self):
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ class ProfileUI(StatsUI):
|
|||||||
|
|
||||||
voting = self.bot.get_cog('TopggCog')
|
voting = self.bot.get_cog('TopggCog')
|
||||||
if voting and not await voting.check_voted_recently(self.userid):
|
if voting and not await voting.check_voted_recently(self.userid):
|
||||||
|
premiumcog = self.bot.get_cog('PremiumCog')
|
||||||
|
if not (premiumcog and await premiumcog.is_premium_guild(self.guild.id)):
|
||||||
self._layout.append((voting.vote_button(),))
|
self._layout.append((voting.vote_button(),))
|
||||||
|
|
||||||
async def _render_stats(self):
|
async def _render_stats(self):
|
||||||
|
|||||||
@@ -753,6 +753,8 @@ class WeeklyMonthlyUI(StatsUI):
|
|||||||
|
|
||||||
voting = self.bot.get_cog('TopggCog')
|
voting = self.bot.get_cog('TopggCog')
|
||||||
if voting and not await voting.check_voted_recently(self.userid):
|
if voting and not await voting.check_voted_recently(self.userid):
|
||||||
|
premiumcog = self.bot.get_cog('PremiumCog')
|
||||||
|
if not (premiumcog and await premiumcog.is_premium_guild(self.guild.id)):
|
||||||
self._layout.append((voting.vote_button(),))
|
self._layout.append((voting.vote_button(),))
|
||||||
|
|
||||||
if self._showing_selector:
|
if self._showing_selector:
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ class Exec(LionCog):
|
|||||||
results = [
|
results = [
|
||||||
appcmd.Choice(name=f"No extensions found matching {partial}", value="None")
|
appcmd.Choice(name=f"No extensions found matching {partial}", value="None")
|
||||||
]
|
]
|
||||||
return results
|
return results[:25]
|
||||||
|
|
||||||
@commands.hybrid_command(
|
@commands.hybrid_command(
|
||||||
name=_('shutdown'),
|
name=_('shutdown'),
|
||||||
|
|||||||
Reference in New Issue
Block a user