refactor (tasklist): Various UI adjustments.
This commit is contained in:
@@ -55,3 +55,5 @@ coin = <:coin:975880967485022239>
|
|||||||
|
|
||||||
task_checked = :🟢:
|
task_checked = :🟢:
|
||||||
task_unchecked = :⚫:
|
task_unchecked = :⚫:
|
||||||
|
task_new = :➕:
|
||||||
|
task_save = :💾:
|
||||||
|
|||||||
@@ -62,13 +62,16 @@ def process_pot(domain, path):
|
|||||||
po.save(targetpo)
|
po.save(targetpo)
|
||||||
po.save_as_mofile(targetmo)
|
po.save_as_mofile(targetmo)
|
||||||
print(f"Processed {entries} from POT {domain}.")
|
print(f"Processed {entries} from POT {domain}.")
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def process_all():
|
def process_all():
|
||||||
|
total = 0
|
||||||
for file in os.scandir(templates):
|
for file in os.scandir(templates):
|
||||||
if file.name.endswith('pot'):
|
if file.name.endswith('pot'):
|
||||||
print(f"Processing pot: {file.name[:-4]}")
|
print(f"Processing pot: {file.name[:-4]}")
|
||||||
process_pot(file.name[:-4], file.path)
|
total += process_pot(file.name[:-4], file.path)
|
||||||
|
print(f"Total strings: {total}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ cmd_map = {
|
|||||||
"cmd_shop": "shop open",
|
"cmd_shop": "shop open",
|
||||||
"cmd_room": "room rent",
|
"cmd_room": "room rent",
|
||||||
"cmd_reminders": "remindme in",
|
"cmd_reminders": "remindme in",
|
||||||
"cmd_tasklist": "tasklist open",
|
"cmd_tasklist": "tasklist",
|
||||||
"cmd_timers": "timers list",
|
"cmd_timers": "timers list",
|
||||||
"cmd_schedule": "schedule book",
|
"cmd_schedule": "schedule",
|
||||||
"cmd_dashboard": "dashboard"
|
"cmd_dashboard": "dashboard"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ class ScheduleCog(LionCog):
|
|||||||
|
|
||||||
slot = self.active_slots.get(slotid, None)
|
slot = self.active_slots.get(slotid, None)
|
||||||
session = slot.sessions.get(guildid, None) if slot else None
|
session = slot.sessions.get(guildid, None) if slot else None
|
||||||
member = session.pop(userid, None) if session else None
|
member = session.members.pop(userid, None) if session else None
|
||||||
if member is not None:
|
if member is not None:
|
||||||
if slot.closing.is_set():
|
if slot.closing.is_set():
|
||||||
# Don't try to cancel a booking for a closing active slot.
|
# Don't try to cancel a booking for a closing active slot.
|
||||||
|
|||||||
@@ -210,16 +210,16 @@ class TimeSlot:
|
|||||||
await batchrun_per_second(coros, 5)
|
await batchrun_per_second(coros, 5)
|
||||||
|
|
||||||
# Save messageids
|
# Save messageids
|
||||||
if sessions:
|
tmptable = TemporaryTable(
|
||||||
tmptable = TemporaryTable(
|
'_gid', '_sid', '_mid',
|
||||||
'_gid', '_sid', '_mid',
|
types=('BIGINT', 'INTEGER', 'BIGINT')
|
||||||
types=('BIGINT', 'INTEGER', 'BIGINT')
|
)
|
||||||
)
|
tmptable.values = [
|
||||||
tmptable.values = [
|
(sg.data.guildid, sg.data.slotid, sg.messageid)
|
||||||
(sg.data.guildid, sg.data.slotid, sg.messageid)
|
for sg in sessions
|
||||||
for sg in sessions
|
if sg.messageid is not None
|
||||||
if sg.messageid is not None
|
]
|
||||||
]
|
if tmptable.values:
|
||||||
await Data.ScheduleSession.table.update_where(
|
await Data.ScheduleSession.table.update_where(
|
||||||
guildid=tmptable['_gid'], slotid=tmptable['_sid']
|
guildid=tmptable['_gid'], slotid=tmptable['_sid']
|
||||||
).set(
|
).set(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from collections import defaultdict
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@@ -9,7 +10,7 @@ from discord.app_commands.transformers import AppCommandOptionType as cmdopt
|
|||||||
from meta import LionBot, LionCog, LionContext
|
from meta import LionBot, LionCog, LionContext
|
||||||
from meta.errors import UserInputError
|
from meta.errors import UserInputError
|
||||||
from utils.lib import utc_now, error_embed
|
from utils.lib import utc_now, error_embed
|
||||||
from utils.ui import ChoicedEnum, Transformed
|
from utils.ui import ChoicedEnum, Transformed, AButton
|
||||||
|
|
||||||
from data import Condition, NULL
|
from data import Condition, NULL
|
||||||
from wards import low_management_ward
|
from wards import low_management_ward
|
||||||
@@ -17,10 +18,10 @@ from wards import low_management_ward
|
|||||||
from . import babel, logger
|
from . import babel, logger
|
||||||
from .data import TasklistData
|
from .data import TasklistData
|
||||||
from .tasklist import Tasklist
|
from .tasklist import Tasklist
|
||||||
from .ui import TasklistUI, SingleEditor, BulkEditor
|
from .ui import TasklistUI, SingleEditor, BulkEditor, TasklistCaller
|
||||||
from .settings import TasklistSettings, TasklistConfigUI
|
from .settings import TasklistSettings, TasklistConfigUI
|
||||||
|
|
||||||
_p = babel._p
|
_p, _np = babel._p, babel._np
|
||||||
|
|
||||||
|
|
||||||
MAX_LENGTH = 100
|
MAX_LENGTH = 100
|
||||||
@@ -128,13 +129,14 @@ class TasklistCog(LionCog):
|
|||||||
self.babel = babel
|
self.babel = babel
|
||||||
self.settings = TasklistSettings()
|
self.settings = TasklistSettings()
|
||||||
|
|
||||||
|
self.live_tasklists = TasklistUI._live_
|
||||||
|
|
||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
await self.data.init()
|
await self.data.init()
|
||||||
self.bot.core.guild_config.register_model_setting(self.settings.task_reward)
|
self.bot.core.guild_config.register_model_setting(self.settings.task_reward)
|
||||||
self.bot.core.guild_config.register_model_setting(self.settings.task_reward_limit)
|
self.bot.core.guild_config.register_model_setting(self.settings.task_reward_limit)
|
||||||
|
self.bot.add_view(TasklistCaller(self.bot))
|
||||||
|
|
||||||
# TODO: Better method for getting single load
|
|
||||||
# Or better, unloading crossloaded group
|
|
||||||
configcog = self.bot.get_cog('ConfigCog')
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
self.crossload_group(self.configure_group, configcog.configure_group)
|
||||||
|
|
||||||
@@ -164,30 +166,164 @@ class TasklistCog(LionCog):
|
|||||||
f"'{amount}' coins for completing '{count}' tasks."
|
f"'{amount}' coins for completing '{count}' tasks."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def is_tasklist_channel(self, channel) -> bool:
|
||||||
|
if not channel.guild:
|
||||||
|
return True
|
||||||
|
channels = (await self.settings.tasklist_channels.get(channel.guild.id)).value
|
||||||
|
return (channel in channels) or (channel.category in channels)
|
||||||
|
|
||||||
|
async def call_tasklist(self, interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
channel = interaction.channel
|
||||||
|
guild = channel.guild
|
||||||
|
userid = interaction.user.id
|
||||||
|
|
||||||
|
tasklist = await Tasklist.fetch(self.bot, self.data, userid)
|
||||||
|
|
||||||
|
if await self.is_tasklist_channel(channel):
|
||||||
|
tasklistui = TasklistUI.fetch(tasklist, channel, guild, timeout=None)
|
||||||
|
await tasklistui.summon(force=True)
|
||||||
|
await interaction.delete_original_response()
|
||||||
|
else:
|
||||||
|
# Note that this will also close any existing listening tasklists in this channel (for this user)
|
||||||
|
tasklistui = TasklistUI.fetch(tasklist, channel, guild, timeout=600)
|
||||||
|
await tasklistui.run(interaction)
|
||||||
|
|
||||||
|
@LionCog.listener('on_tasklist_update')
|
||||||
|
async def update_listening_tasklists(self, userid, channel=None, summon=True):
|
||||||
|
"""
|
||||||
|
Propagate a tasklist update to all persistent tasklist UIs for this user.
|
||||||
|
|
||||||
|
If channel is given, also summons the UI if the channel is a tasklist channel.
|
||||||
|
"""
|
||||||
|
# Do the given channel first, and summon if requested
|
||||||
|
if channel and (tui := TasklistUI._live_[userid].get(channel.id, None)) is not None:
|
||||||
|
try:
|
||||||
|
if summon and await self.is_tasklist_channel(channel):
|
||||||
|
await tui.summon()
|
||||||
|
else:
|
||||||
|
await tui.refresh()
|
||||||
|
await tui.redraw()
|
||||||
|
except discord.HTTPException:
|
||||||
|
await tui.close()
|
||||||
|
|
||||||
|
# Now do the rest of the listening channels
|
||||||
|
listening = TasklistUI._live_[userid]
|
||||||
|
for cid, ui in listening.items():
|
||||||
|
if channel and channel.id == cid:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
await ui.refresh()
|
||||||
|
await ui.redraw()
|
||||||
|
except discord.HTTPException:
|
||||||
|
await tui.close()
|
||||||
|
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name=_p('cmd:tasklist', "tasklist"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:tasklist|desc',
|
||||||
|
"Open your tasklist."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def tasklist_cmd(self, ctx: LionContext):
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
await self.call_tasklist(ctx.interaction)
|
||||||
|
|
||||||
@cmds.hybrid_group(
|
@cmds.hybrid_group(
|
||||||
name=_p('group:tasklist', "tasklist")
|
name=_p('group:tasks', "tasks"),
|
||||||
|
description=_p('group:tasks|desc', "Base command group for tasklist commands.")
|
||||||
)
|
)
|
||||||
async def tasklist_group(self, ctx: LionContext):
|
async def tasklist_group(self, ctx: LionContext):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def task_acmpl(self, interaction: discord.Interaction, partial: str) -> list[appcmds.Choice]:
|
async def _task_acmpl(self, userid: int, partial: str, multi=False) -> list[appcmds.Choice]:
|
||||||
|
"""
|
||||||
|
Generate a list of task Choices matching a given partial string.
|
||||||
|
|
||||||
|
Supports single and multiple task matching.
|
||||||
|
"""
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
|
|
||||||
# Should usually be cached, so this won't trigger repetitive db access
|
# Should usually be cached, so this won't trigger repetitive db access
|
||||||
tasklist = await Tasklist.fetch(self.bot, self.data, interaction.user.id)
|
tasklist = await Tasklist.fetch(self.bot, self.data, userid)
|
||||||
|
|
||||||
|
# Special case for an empty tasklist
|
||||||
|
if not tasklist.tasklist:
|
||||||
|
return [
|
||||||
|
appcmds.Choice(
|
||||||
|
name=t(_p(
|
||||||
|
'argtype:taskid|error:no_tasks',
|
||||||
|
"Tasklist empty! No matching tasks."
|
||||||
|
)),
|
||||||
|
value=partial
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
labels = []
|
labels = []
|
||||||
|
idmap = {}
|
||||||
for label, task in tasklist.labelled.items():
|
for label, task in tasklist.labelled.items():
|
||||||
labelstring = '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
labelstring = '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
||||||
taskstring = f"{labelstring} {task.content}"
|
taskstring = f"{labelstring} {task.content}"
|
||||||
|
idmap[task.taskid] = labelstring
|
||||||
labels.append((labelstring, taskstring))
|
labels.append((labelstring, taskstring))
|
||||||
|
|
||||||
|
# Assume user is typing a label
|
||||||
matching = [(label, task) for label, task in labels if label.startswith(partial)]
|
matching = [(label, task) for label, task in labels if label.startswith(partial)]
|
||||||
|
|
||||||
|
# If partial does match any labels, search for partial in task content
|
||||||
if not matching:
|
if not matching:
|
||||||
matching = [(label, task) for label, task in labels if partial.lower() in task.lower()]
|
matching = [(label, task) for label, task in labels if partial.lower() in task.lower()]
|
||||||
|
|
||||||
if not matching:
|
if matching:
|
||||||
|
# If matches were found, assume user wants one of the matches
|
||||||
|
options = [
|
||||||
|
appcmds.Choice(name=task_string, value=label)
|
||||||
|
for label, task_string in matching
|
||||||
|
]
|
||||||
|
elif multi and (',' in partial or '-' in partial):
|
||||||
|
# Try parsing input as a multi-list
|
||||||
|
try:
|
||||||
|
parsed = tasklist.parse_labels(partial)
|
||||||
|
multi_name = ', '.join(idmap[tid] for tid in parsed)
|
||||||
|
if len(multi_name) > 100:
|
||||||
|
multi_name = multi_name[:96]
|
||||||
|
multi_name, _ = multi_name.rsplit(',', maxsplit=1)
|
||||||
|
multi_name = multi_name + ', ...'
|
||||||
|
except UserInputError as e:
|
||||||
|
parsed = []
|
||||||
|
error = t(_p(
|
||||||
|
'argtype:taskid|error:parse_multi',
|
||||||
|
"(Warning: {error})"
|
||||||
|
)).format(
|
||||||
|
error=e.msg
|
||||||
|
)
|
||||||
|
remaining = 100 - len(error)
|
||||||
|
multi_name = f"{partial[:remaining-1]} {error}"
|
||||||
|
|
||||||
|
multi_option = appcmds.Choice(
|
||||||
|
name=multi_name,
|
||||||
|
value=partial
|
||||||
|
)
|
||||||
|
options = [multi_option]
|
||||||
|
# Regardless of parse status, show matches with last split, if they exist.
|
||||||
|
if ',' in partial:
|
||||||
|
_, last_split = partial.rsplit(',', maxsplit=1)
|
||||||
|
else:
|
||||||
|
last_split = partial
|
||||||
|
if '-' in last_split:
|
||||||
|
_, last_split = last_split.rsplit('-', maxsplit=1)
|
||||||
|
last_split = last_split.strip(' ')
|
||||||
|
else:
|
||||||
|
last_split = last_split.strip(' ')
|
||||||
|
matching = [(label, task) for label, task in labels if label.startswith(last_split)]
|
||||||
|
if not matching:
|
||||||
|
matching = [(label, task) for label, task in labels if last_split.lower() in task.lower()]
|
||||||
|
options.extend(
|
||||||
|
appcmds.Choice(name=task_string, value=label)
|
||||||
|
for label, task_string in matching
|
||||||
|
)
|
||||||
|
else:
|
||||||
options = [
|
options = [
|
||||||
appcmds.Choice(
|
appcmds.Choice(
|
||||||
name=t(_p(
|
name=t(_p(
|
||||||
@@ -197,73 +333,34 @@ class TasklistCog(LionCog):
|
|||||||
value=partial
|
value=partial
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
else:
|
|
||||||
options = [
|
|
||||||
appcmds.Choice(name=task_string, value=label)
|
|
||||||
for label, task_string in matching
|
|
||||||
]
|
|
||||||
return options[:25]
|
return options[:25]
|
||||||
|
|
||||||
async def is_tasklist_channel(self, channel) -> bool:
|
async def task_acmpl(self, interaction: discord.Interaction, partial: str) -> list[appcmds.Choice]:
|
||||||
if not channel.guild:
|
"""
|
||||||
return True
|
Shared autocomplete for single task parameters.
|
||||||
channels = (await self.settings.tasklist_channels.get(channel.guild.id)).value
|
"""
|
||||||
return (not channels) or (channel in channels) or (channel.category in channels)
|
return await self._task_acmpl(interaction.user.id, partial, multi=False)
|
||||||
|
|
||||||
|
async def tasks_acmpl(self, interaction: discord.Interaction, partial: str) -> list[appcmds.Choice]:
|
||||||
|
"""
|
||||||
|
Shared autocomplete for multiple task parameters.
|
||||||
|
"""
|
||||||
|
return await self._task_acmpl(interaction.user.id, partial, multi=True)
|
||||||
|
|
||||||
@tasklist_group.command(
|
@tasklist_group.command(
|
||||||
name=_p('cmd:tasklist_open', "open"),
|
name=_p('cmd:tasks_new', "new"),
|
||||||
description=_p(
|
description=_p(
|
||||||
'cmd:tasklist_open|desc',
|
'cmd:tasks_new|desc',
|
||||||
"Open your tasklist."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
async def tasklist_open_cmd(self, ctx: LionContext):
|
|
||||||
# TODO: Further arguments for style, e.g. gui/block/text
|
|
||||||
if await self.is_tasklist_channel(ctx.channel):
|
|
||||||
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
|
|
||||||
tasklist = await Tasklist.fetch(self.bot, self.data, ctx.author.id)
|
|
||||||
tasklistui = await TasklistUI.fetch(tasklist, ctx.channel, ctx.guild)
|
|
||||||
await tasklistui.summon()
|
|
||||||
await ctx.interaction.delete_original_response()
|
|
||||||
else:
|
|
||||||
t = self.bot.translator.t
|
|
||||||
channels = (await self.settings.tasklist_channels.get(ctx.guild.id)).value
|
|
||||||
viewable = [
|
|
||||||
channel for channel in channels
|
|
||||||
if (channel.permissions_for(ctx.author).send_messages
|
|
||||||
or channel.permissions_for(ctx.author).send_messages_in_threads)
|
|
||||||
]
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=t(_p('cmd:tasklist_open|error:tasklist_channel|title', "Sorry, I can't do that here")),
|
|
||||||
colour=discord.Colour.brand_red()
|
|
||||||
)
|
|
||||||
if viewable:
|
|
||||||
embed.description = t(_p(
|
|
||||||
'cmd:tasklist_open|error:tasklist_channel|desc',
|
|
||||||
"Please use direct messages or one of the following channels "
|
|
||||||
"or categories for managing your tasks:\n{channels}"
|
|
||||||
)).format(channels='\n'.join(channel.mention for channel in viewable))
|
|
||||||
else:
|
|
||||||
embed.description = t(_p(
|
|
||||||
'cmd:tasklist_open|error:tasklist_channel|desc',
|
|
||||||
"There are no channels available here where you may open your tasklist!"
|
|
||||||
))
|
|
||||||
await ctx.reply(embed=embed, ephemeral=True)
|
|
||||||
|
|
||||||
@tasklist_group.command(
|
|
||||||
name=_p('cmd:tasklist_new', "new"),
|
|
||||||
description=_p(
|
|
||||||
'cmd:tasklist_new|desc',
|
|
||||||
"Add a new task to your tasklist."
|
"Add a new task to your tasklist."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@appcmds.rename(
|
@appcmds.rename(
|
||||||
content=_p('cmd:tasklist_new|param:content', "task"),
|
content=_p('cmd:tasks_new|param:content', "task"),
|
||||||
parent=_p('cmd:tasklist_new|param:parent', 'parent')
|
parent=_p('cmd:tasks_new|param:parent', 'parent')
|
||||||
)
|
)
|
||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
content=_p('cmd:tasklist_new|param:content|desc', "Content of your new task."),
|
content=_p('cmd:tasks_new|param:content|desc', "Content of your new task."),
|
||||||
parent=_p('cmd:tasklist_new|param:parent', 'Parent of this task.')
|
parent=_p('cmd:tasks_new|param:parent', 'Parent of this task.')
|
||||||
)
|
)
|
||||||
async def tasklist_new_cmd(self, ctx: LionContext,
|
async def tasklist_new_cmd(self, ctx: LionContext,
|
||||||
content: appcmds.Range[str, 1, MAX_LENGTH],
|
content: appcmds.Range[str, 1, MAX_LENGTH],
|
||||||
@@ -282,52 +379,49 @@ class TasklistCog(LionCog):
|
|||||||
await ctx.interaction.edit_original_response(
|
await ctx.interaction.edit_original_response(
|
||||||
embed=error_embed(
|
embed=error_embed(
|
||||||
t(_p(
|
t(_p(
|
||||||
'cmd:tasklist_new|error:parse_parent',
|
'cmd:tasks_new|error:parse_parent',
|
||||||
"Could not find task number `{input}` in your tasklist."
|
"Could not find parent task number `{input}` in your tasklist."
|
||||||
)).format(input=parent)
|
)).format(input=parent)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create task
|
# Create task
|
||||||
await tasklist.create_task(content, parentid=pid)
|
task = await tasklist.create_task(content, parentid=pid)
|
||||||
|
|
||||||
if await self.is_tasklist_channel(ctx.interaction.channel):
|
# Ack creation
|
||||||
# summon tasklist
|
label = tasklist.labelid(task.taskid)
|
||||||
tasklistui = await TasklistUI.fetch(tasklist, ctx.channel, ctx.guild)
|
embed = discord.Embed(
|
||||||
await tasklistui.summon()
|
colour=discord.Colour.brand_green(),
|
||||||
await ctx.interaction.delete_original_response()
|
description=t(_p(
|
||||||
else:
|
'cmd:tasks_new|resp:success',
|
||||||
# ack creation
|
"{tick} Created task `{label}`."
|
||||||
embed = discord.Embed(
|
)).format(tick=self.bot.config.emojis.tick, label=tasklist.format_label(label))
|
||||||
colour=discord.Colour.brand_green(),
|
)
|
||||||
description=t(_p(
|
await ctx.interaction.edit_original_response(
|
||||||
'cmd:tasklist_new|resp:success',
|
embed=embed,
|
||||||
"{tick} Task created successfully."
|
view=None if ctx.channel.id in TasklistUI._live_[ctx.author.id] else TasklistCaller(self.bot)
|
||||||
)).format(tick=self.bot.config.emojis.tick)
|
)
|
||||||
)
|
self.bot.dispatch('tasklist_update', userid=ctx.author.id, channel=ctx.channel)
|
||||||
await ctx.interaction.edit_original_response(embed=embed)
|
|
||||||
|
|
||||||
@tasklist_new_cmd.autocomplete('parent')
|
tasklist_new_cmd.autocomplete('parent')(task_acmpl)
|
||||||
async def tasklist_new_cmd_parent_acmpl(self, interaction: discord.Interaction, partial: str):
|
|
||||||
return await self.task_acmpl(interaction, partial)
|
|
||||||
|
|
||||||
@tasklist_group.command(
|
@tasklist_group.command(
|
||||||
name=_p('cmd:tasklist_edit', "edit"),
|
name=_p('cmd:tasks_edit', "edit"),
|
||||||
description=_p(
|
description=_p(
|
||||||
'cmd:tasklist_edit|desc',
|
'cmd:tasks_edit|desc',
|
||||||
"Edit tasks in your tasklist."
|
"Edit a task in your tasklist."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@appcmds.rename(
|
@appcmds.rename(
|
||||||
taskstr=_p('cmd:tasklist_edit|param:taskstr', "task"),
|
taskstr=_p('cmd:tasks_edit|param:taskstr', "task"),
|
||||||
new_content=_p('cmd:tasklist_edit|param:new_content', "new_task"),
|
new_content=_p('cmd:tasks_edit|param:new_content', "new_task"),
|
||||||
new_parent=_p('cmd:tasklist_edit|param:new_parent', "new_parent"),
|
new_parent=_p('cmd:tasks_edit|param:new_parent', "new_parent"),
|
||||||
)
|
)
|
||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
taskstr=_p('cmd:tasklist_edit|param:taskstr|desc', "Which task do you want to update?"),
|
taskstr=_p('cmd:tasks_edit|param:taskstr|desc', "Which task do you want to update?"),
|
||||||
new_content=_p('cmd:tasklist_edit|param:new_content|desc', "What do you want to change the task to?"),
|
new_content=_p('cmd:tasks_edit|param:new_content|desc', "What do you want to change the task to?"),
|
||||||
new_parent=_p('cmd:tasklist_edit|param:new_parent|desc', "Which task do you want to be the new parent?"),
|
new_parent=_p('cmd:tasks_edit|param:new_parent|desc', "Which task do you want to be the new parent?"),
|
||||||
)
|
)
|
||||||
async def tasklist_edit_cmd(self, ctx: LionContext,
|
async def tasklist_edit_cmd(self, ctx: LionContext,
|
||||||
taskstr: str,
|
taskstr: str,
|
||||||
@@ -345,8 +439,8 @@ class TasklistCog(LionCog):
|
|||||||
await ctx.interaction.response.send_message(
|
await ctx.interaction.response.send_message(
|
||||||
embed=error_embed(
|
embed=error_embed(
|
||||||
t(_p(
|
t(_p(
|
||||||
'cmd:tasklist_edit|error:parse_taskstr',
|
'cmd:tasks_edit|error:parse_taskstr',
|
||||||
"Could not find task number `{input}` in your tasklist."
|
"Could not find target task number `{input}` in your tasklist."
|
||||||
)).format(input=taskstr)
|
)).format(input=taskstr)
|
||||||
),
|
),
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
@@ -361,8 +455,8 @@ class TasklistCog(LionCog):
|
|||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
embed=error_embed(
|
embed=error_embed(
|
||||||
t(_p(
|
t(_p(
|
||||||
'cmd:tasklist_edit|error:parse_parent',
|
'cmd:tasks_edit|error:parse_parent',
|
||||||
"Could not find task number `{input}` in your tasklist."
|
"Could not find new parent task number `{input}` in your tasklist."
|
||||||
)).format(input=new_parent)
|
)).format(input=new_parent)
|
||||||
),
|
),
|
||||||
ephemeral=True
|
ephemeral=True
|
||||||
@@ -377,25 +471,22 @@ class TasklistCog(LionCog):
|
|||||||
if args:
|
if args:
|
||||||
await tasklist.update_tasks(tid, **args)
|
await tasklist.update_tasks(tid, **args)
|
||||||
|
|
||||||
if await self.is_tasklist_channel(ctx.channel):
|
embed = discord.Embed(
|
||||||
tasklistui = await TasklistUI.fetch(tasklist, ctx.channel, ctx.guild)
|
colour=discord.Color.brand_green(),
|
||||||
await tasklistui.summon()
|
description=t(_p(
|
||||||
else:
|
'cmd:tasks_edit|resp:success|desc',
|
||||||
embed = discord.Embed(
|
"{tick} Task `{label}` updated."
|
||||||
colour=discord.Color.brand_green(),
|
)).format(tick=self.bot.config.emojis.tick, label=tasklist.format_label(tasklist.labelid(tid))),
|
||||||
description=t(_p(
|
)
|
||||||
'cmd:tasklist_edit|resp:success|desc',
|
await ctx.interaction.edit_original_response(
|
||||||
"{tick} Task updated successfully."
|
embed=embed,
|
||||||
)).format(tick=self.bot.config.emojis.tick),
|
view=None if ctx.channel.id in TasklistUI._live_[ctx.author.id] else TasklistCaller(self.bot)
|
||||||
)
|
)
|
||||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
self.bot.dispatch('tasklist_update', userid=ctx.author.id, channel=ctx.channel)
|
||||||
|
|
||||||
if new_content or new_parent:
|
if new_content or new_parent:
|
||||||
# Manual edit route
|
# Manual edit route
|
||||||
await handle_update(ctx.interaction, new_content, new_parent)
|
await handle_update(ctx.interaction, new_content, new_parent)
|
||||||
if not ctx.interaction.response.is_done():
|
|
||||||
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
|
|
||||||
await ctx.interaction.delete_original_response()
|
|
||||||
else:
|
else:
|
||||||
# Modal edit route
|
# Modal edit route
|
||||||
task = tasklist.tasklist[tid]
|
task = tasklist.tasklist[tid]
|
||||||
@@ -410,22 +501,15 @@ class TasklistCog(LionCog):
|
|||||||
@editor.submit_callback()
|
@editor.submit_callback()
|
||||||
async def update_task(interaction: discord.Interaction):
|
async def update_task(interaction: discord.Interaction):
|
||||||
await handle_update(interaction, editor.task.value, editor.parent.value)
|
await handle_update(interaction, editor.task.value, editor.parent.value)
|
||||||
if not interaction.response.is_done():
|
|
||||||
await interaction.response.defer()
|
|
||||||
|
|
||||||
await ctx.interaction.response.send_modal(editor)
|
await ctx.interaction.response.send_modal(editor)
|
||||||
|
|
||||||
@tasklist_edit_cmd.autocomplete('taskstr')
|
tasklist_edit_cmd.autocomplete('taskstr')(task_acmpl)
|
||||||
async def tasklist_edit_cmd_taskstr_acmpl(self, interaction: discord.Interaction, partial: str):
|
tasklist_edit_cmd.autocomplete('new_parent')(task_acmpl)
|
||||||
return await self.task_acmpl(interaction, partial)
|
|
||||||
|
|
||||||
@tasklist_edit_cmd.autocomplete('new_parent')
|
|
||||||
async def tasklist_edit_cmd_new_parent_acmpl(self, interaction: discord.Interaction, partial: str):
|
|
||||||
return await self.task_acmpl(interaction, partial)
|
|
||||||
|
|
||||||
@tasklist_group.command(
|
@tasklist_group.command(
|
||||||
name=_p('cmd:tasklist_clear', "clear"),
|
name=_p('cmd:tasks_clear', "clear"),
|
||||||
description=_p('cmd:tasklist_clear|desc', "Clear your tasklist.")
|
description=_p('cmd:tasks_clear|desc', "Clear your tasklist.")
|
||||||
)
|
)
|
||||||
async def tasklist_clear_cmd(self, ctx: LionContext):
|
async def tasklist_clear_cmd(self, ctx: LionContext):
|
||||||
t = ctx.bot.translator.t
|
t = ctx.bot.translator.t
|
||||||
@@ -434,47 +518,47 @@ class TasklistCog(LionCog):
|
|||||||
await tasklist.update_tasklist(deleted_at=utc_now())
|
await tasklist.update_tasklist(deleted_at=utc_now())
|
||||||
await ctx.reply(
|
await ctx.reply(
|
||||||
t(_p(
|
t(_p(
|
||||||
'cmd:tasklist_clear|resp:success',
|
'cmd:tasks_clear|resp:success',
|
||||||
"Your tasklist has been cleared."
|
"Your tasklist has been cleared."
|
||||||
)),
|
)),
|
||||||
|
view=None if ctx.channel.id in TasklistUI._live_[ctx.author.id] else TasklistCaller(self.bot),
|
||||||
ephemeral=True
|
ephemeral=True
|
||||||
)
|
)
|
||||||
tasklistui = await TasklistUI.fetch(tasklist, ctx.channel, ctx.guild)
|
self.bot.dispatch('tasklist_update', userid=ctx.author.id, channel=ctx.channel)
|
||||||
await tasklistui.summon()
|
|
||||||
|
|
||||||
@tasklist_group.command(
|
@tasklist_group.command(
|
||||||
name=_p('cmd:tasklist_remove', "remove"),
|
name=_p('cmd:tasks_remove', "remove"),
|
||||||
description=_p(
|
description=_p(
|
||||||
'cmd:tasklist_remove|desc',
|
'cmd:tasks_remove|desc',
|
||||||
"Remove tasks matching all the provided conditions. (E.g. remove tasks completed before today)."
|
"Remove tasks matching all the provided conditions. (E.g. remove tasks completed before today)."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@appcmds.rename(
|
@appcmds.rename(
|
||||||
taskidstr=_p('cmd:tasklist_remove|param:taskidstr', "tasks"),
|
taskidstr=_p('cmd:tasks_remove|param:taskidstr', "tasks"),
|
||||||
created_before=_p('cmd:tasklist_remove|param:created_before', "created_before"),
|
created_before=_p('cmd:tasks_remove|param:created_before', "created_before"),
|
||||||
updated_before=_p('cmd:tasklist_remove|param:updated_before', "updated_before"),
|
updated_before=_p('cmd:tasks_remove|param:updated_before', "updated_before"),
|
||||||
completed=_p('cmd:tasklist_remove|param:completed', "completed"),
|
completed=_p('cmd:tasks_remove|param:completed', "completed"),
|
||||||
cascade=_p('cmd:tasklist_remove|param:cascade', "cascade")
|
cascade=_p('cmd:tasks_remove|param:cascade', "cascade")
|
||||||
)
|
)
|
||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
taskidstr=_p(
|
taskidstr=_p(
|
||||||
'cmd:tasklist_remove|param:taskidstr|desc',
|
'cmd:tasks_remove|param:taskidstr|desc',
|
||||||
"List of task numbers or ranges to remove (e.g. 1, 2, 5-7, 8.1-3, 9-)."
|
"List of task numbers or ranges to remove (e.g. 1, 2, 5-7, 8.1-3, 9-)."
|
||||||
),
|
),
|
||||||
created_before=_p(
|
created_before=_p(
|
||||||
'cmd:tasklist_remove|param:created_before|desc',
|
'cmd:tasks_remove|param:created_before|desc',
|
||||||
"Only delete tasks created before the selected time."
|
"Only delete tasks created before the selected time."
|
||||||
),
|
),
|
||||||
updated_before=_p(
|
updated_before=_p(
|
||||||
'cmd:tasklist_remove|param:updated_before|desc',
|
'cmd:tasks_remove|param:updated_before|desc',
|
||||||
"Only deleted tasks update (i.e. completed or edited) before the selected time."
|
"Only deleted tasks update (i.e. completed or edited) before the selected time."
|
||||||
),
|
),
|
||||||
completed=_p(
|
completed=_p(
|
||||||
'cmd:tasklist_remove|param:completed',
|
'cmd:tasks_remove|param:completed',
|
||||||
"Only delete tasks which are (not) complete."
|
"Only delete tasks which are (not) complete."
|
||||||
),
|
),
|
||||||
cascade=_p(
|
cascade=_p(
|
||||||
'cmd:tasklist_remove|param:cascade',
|
'cmd:tasks_remove|param:cascade',
|
||||||
"Whether to recursively remove subtasks of removed tasks."
|
"Whether to recursively remove subtasks of removed tasks."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -506,7 +590,7 @@ class TasklistCog(LionCog):
|
|||||||
# Explicitly error if none of the ranges matched
|
# Explicitly error if none of the ranges matched
|
||||||
await ctx.interaction.edit_original_response(
|
await ctx.interaction.edit_original_response(
|
||||||
embed=error_embed(
|
embed=error_embed(
|
||||||
'cmd:tasklist_remove_cmd|error:no_matching',
|
'cmd:tasks_remove_cmd|error:no_matching',
|
||||||
"No tasks on your tasklist match `{input}`"
|
"No tasks on your tasklist match `{input}`"
|
||||||
).format(input=taskidstr)
|
).format(input=taskidstr)
|
||||||
)
|
)
|
||||||
@@ -515,8 +599,7 @@ class TasklistCog(LionCog):
|
|||||||
conditions.append(self.data.Task.taskid == taskids)
|
conditions.append(self.data.Task.taskid == taskids)
|
||||||
|
|
||||||
if created_before is not None or updated_before is not None:
|
if created_before is not None or updated_before is not None:
|
||||||
# TODO: Extract timezone from user settings
|
timezone = ctx.alion.timezone
|
||||||
timezone = None
|
|
||||||
if created_before is not None:
|
if created_before is not None:
|
||||||
conditions.append(self.data.Task.created_at <= created_before.cutoff(timezone))
|
conditions.append(self.data.Task.created_at <= created_before.cutoff(timezone))
|
||||||
if updated_before is not None:
|
if updated_before is not None:
|
||||||
@@ -531,46 +614,52 @@ class TasklistCog(LionCog):
|
|||||||
if not tasks:
|
if not tasks:
|
||||||
await ctx.interaction.edit_original_response(
|
await ctx.interaction.edit_original_response(
|
||||||
embed=error_embed(
|
embed=error_embed(
|
||||||
'cmd:tasklist_remove_cmd|error:no_matching',
|
'cmd:tasks_remove_cmd|error:no_matching',
|
||||||
"No tasks on your tasklist matching all the given conditions!"
|
"No tasks on your tasklist matching all the given conditions!"
|
||||||
).format(input=taskidstr)
|
).format(input=taskidstr)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
taskids = [task.taskid for task in tasks]
|
taskids = [task.taskid for task in tasks]
|
||||||
|
label = tasklist.format_label(tasklist.labelid(taskids[0]))
|
||||||
await tasklist.update_tasks(*taskids, cascade=cascade, deleted_at=utc_now())
|
await tasklist.update_tasks(*taskids, cascade=cascade, deleted_at=utc_now())
|
||||||
|
|
||||||
# Ack changes or summon tasklist
|
# Ack changes and summon tasklist
|
||||||
if await self.is_tasklist_channel(ctx.channel):
|
embed = discord.Embed(
|
||||||
# Summon tasklist
|
colour=discord.Colour.brand_green(),
|
||||||
tasklistui = await TasklistUI.fetch(tasklist, ctx.channel, ctx.guild)
|
description=t(_np(
|
||||||
await tasklistui.summon()
|
'cmd:tasks_remove|resp:success',
|
||||||
await ctx.interaction.delete_original_response()
|
"{tick} Deleted task `{label}`",
|
||||||
else:
|
"{tick} Deleted `{count}` tasks from your tasklist.",
|
||||||
# Ack deletion
|
len(taskids)
|
||||||
embed = discord.Embed(
|
)).format(
|
||||||
colour=discord.Colour.brand_green(),
|
tick=self.bot.config.emojis.tick,
|
||||||
description=t(_p(
|
label=label,
|
||||||
'cmd:tasklist_remove|resp:success',
|
count=len(taskids)
|
||||||
"{tick} tasks deleted."
|
|
||||||
)).format(tick=self.bot.config.emojis.tick)
|
|
||||||
)
|
)
|
||||||
await ctx.interaction.edit_original_response(embed=embed)
|
)
|
||||||
|
await ctx.interaction.edit_original_response(
|
||||||
|
embed=embed,
|
||||||
|
view=None if ctx.channel.id in TasklistUI._live_[ctx.author.id] else TasklistCaller(self.bot)
|
||||||
|
)
|
||||||
|
self.bot.dispatch('tasklist_update', userid=ctx.author.id, channel=ctx.channel)
|
||||||
|
|
||||||
|
tasklist_remove_cmd.autocomplete('taskidstr')(tasks_acmpl)
|
||||||
|
|
||||||
@tasklist_group.command(
|
@tasklist_group.command(
|
||||||
name=_p('cmd:tasklist_tick', "tick"),
|
name=_p('cmd:tasks_tick', "tick"),
|
||||||
description=_p('cmd:tasklist_tick|desc', "Mark the given tasks as completed.")
|
description=_p('cmd:tasks_tick|desc', "Mark the given tasks as completed.")
|
||||||
)
|
)
|
||||||
@appcmds.rename(
|
@appcmds.rename(
|
||||||
taskidstr=_p('cmd:tasklist_tick|param:taskidstr', "tasks"),
|
taskidstr=_p('cmd:tasks_tick|param:taskidstr', "tasks"),
|
||||||
cascade=_p('cmd:tasklist_tick|param:cascade', "cascade")
|
cascade=_p('cmd:tasks_tick|param:cascade', "cascade")
|
||||||
)
|
)
|
||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
taskidstr=_p(
|
taskidstr=_p(
|
||||||
'cmd:tasklist_tick|param:taskidstr|desc',
|
'cmd:tasks_tick|param:taskidstr|desc',
|
||||||
"List of task numbers or ranges to remove (e.g. 1, 2, 5-7, 8.1-3, 9-)."
|
"List of task numbers or ranges to remove (e.g. 1, 2, 5-7, 8.1-3, 9-)."
|
||||||
),
|
),
|
||||||
cascade=_p(
|
cascade=_p(
|
||||||
'cmd:tasklist_tick|param:cascade|desc',
|
'cmd:tasks_tick|param:cascade|desc',
|
||||||
"Whether to also mark all subtasks as complete."
|
"Whether to also mark all subtasks as complete."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -596,7 +685,7 @@ class TasklistCog(LionCog):
|
|||||||
# Explicitly error if none of the ranges matched
|
# Explicitly error if none of the ranges matched
|
||||||
await ctx.interaction.edit_original_response(
|
await ctx.interaction.edit_original_response(
|
||||||
embed=error_embed(
|
embed=error_embed(
|
||||||
'cmd:tasklist_remove_cmd|error:no_matching',
|
'cmd:tasks_remove_cmd|error:no_matching',
|
||||||
"No tasks on your tasklist match `{input}`"
|
"No tasks on your tasklist match `{input}`"
|
||||||
).format(input=taskidstr)
|
).format(input=taskidstr)
|
||||||
)
|
)
|
||||||
@@ -610,38 +699,43 @@ class TasklistCog(LionCog):
|
|||||||
if ctx.guild:
|
if ctx.guild:
|
||||||
self.bot.dispatch('tasks_completed', ctx.author, *taskids)
|
self.bot.dispatch('tasks_completed', ctx.author, *taskids)
|
||||||
|
|
||||||
# Ack changes or summon tasklist
|
# Ack changes and summon tasklist
|
||||||
if await self.is_tasklist_channel(ctx.channel):
|
embed = discord.Embed(
|
||||||
# Summon tasklist
|
colour=discord.Colour.brand_green(),
|
||||||
tasklistui = await TasklistUI.fetch(tasklist, ctx.channel, ctx.guild)
|
description=t(_np(
|
||||||
await tasklistui.summon()
|
'cmd:tasks_tick|resp:success',
|
||||||
await ctx.interaction.delete_original_response()
|
"{tick} Marked `{label}` as complete.",
|
||||||
else:
|
"{tick} Marked `{count}` tasks as complete.",
|
||||||
# Ack edit
|
len(taskids)
|
||||||
embed = discord.Embed(
|
)).format(
|
||||||
colour=discord.Colour.brand_green(),
|
tick=self.bot.config.emojis.tick,
|
||||||
description=t(_p(
|
count=len(taskids),
|
||||||
'cmd:tasklist_tick|resp:success',
|
label=tasklist.format_label(tasklist.labelid(taskids[0])) if taskids else '-'
|
||||||
"{tick} tasks marked as complete."
|
|
||||||
)).format(tick=self.bot.config.emojis.tick)
|
|
||||||
)
|
)
|
||||||
await ctx.interaction.edit_original_response(embed=embed)
|
)
|
||||||
|
await ctx.interaction.edit_original_response(
|
||||||
|
embed=embed,
|
||||||
|
view=None if ctx.channel.id in TasklistUI._live_[ctx.author.id] else TasklistCaller(self.bot)
|
||||||
|
)
|
||||||
|
self.bot.dispatch('tasklist_update', userid=ctx.author.id, channel=ctx.channel)
|
||||||
|
|
||||||
|
tasklist_tick_cmd.autocomplete('taskidstr')(tasks_acmpl)
|
||||||
|
|
||||||
@tasklist_group.command(
|
@tasklist_group.command(
|
||||||
name=_p('cmd:tasklist_untick', "untick"),
|
name=_p('cmd:tasks_untick', "untick"),
|
||||||
description=_p('cmd:tasklist_untick|desc', "Mark the given tasks as incomplete.")
|
description=_p('cmd:tasks_untick|desc', "Mark the given tasks as incomplete.")
|
||||||
)
|
)
|
||||||
@appcmds.rename(
|
@appcmds.rename(
|
||||||
taskidstr=_p('cmd:tasklist_untick|param:taskidstr', "taskids"),
|
taskidstr=_p('cmd:tasks_untick|param:taskidstr', "taskids"),
|
||||||
cascade=_p('cmd:tasklist_untick|param:cascade', "cascade")
|
cascade=_p('cmd:tasks_untick|param:cascade', "cascade")
|
||||||
)
|
)
|
||||||
@appcmds.describe(
|
@appcmds.describe(
|
||||||
taskidstr=_p(
|
taskidstr=_p(
|
||||||
'cmd:tasklist_untick|param:taskidstr|desc',
|
'cmd:tasks_untick|param:taskidstr|desc',
|
||||||
"List of task numbers or ranges to remove (e.g. 1, 2, 5-7, 8.1-3, 9-)."
|
"List of task numbers or ranges to remove (e.g. 1, 2, 5-7, 8.1-3, 9-)."
|
||||||
),
|
),
|
||||||
cascade=_p(
|
cascade=_p(
|
||||||
'cmd:tasklist_untick|param:cascade|desc',
|
'cmd:tasks_untick|param:cascade|desc',
|
||||||
"Whether to also mark all subtasks as incomplete."
|
"Whether to also mark all subtasks as incomplete."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -666,7 +760,7 @@ class TasklistCog(LionCog):
|
|||||||
# Explicitly error if none of the ranges matched
|
# Explicitly error if none of the ranges matched
|
||||||
await ctx.interaction.edit_original_response(
|
await ctx.interaction.edit_original_response(
|
||||||
embed=error_embed(
|
embed=error_embed(
|
||||||
'cmd:tasklist_remove_cmd|error:no_matching',
|
'cmd:tasks_remove_cmd|error:no_matching',
|
||||||
"No tasks on your tasklist match `{input}`"
|
"No tasks on your tasklist match `{input}`"
|
||||||
).format(input=taskidstr)
|
).format(input=taskidstr)
|
||||||
)
|
)
|
||||||
@@ -678,22 +772,27 @@ class TasklistCog(LionCog):
|
|||||||
if taskids:
|
if taskids:
|
||||||
await tasklist.update_tasks(*taskids, cascade=cascade, completed_at=None)
|
await tasklist.update_tasks(*taskids, cascade=cascade, completed_at=None)
|
||||||
|
|
||||||
# Ack changes or summon tasklist
|
# Ack changes and summon tasklist
|
||||||
if await self.is_tasklist_channel(ctx.channel):
|
embed = discord.Embed(
|
||||||
# Summon tasklist
|
colour=discord.Colour.brand_green(),
|
||||||
tasklistui = await TasklistUI.fetch(tasklist, ctx.channel, ctx.guild)
|
description=t(_np(
|
||||||
await tasklistui.summon()
|
'cmd:tasks_untick|resp:success',
|
||||||
await ctx.interaction.delete_original_response()
|
"{tick} Marked `{label}` as incomplete.",
|
||||||
else:
|
"{tick} Marked `{count}` tasks as incomplete.",
|
||||||
# Ack edit
|
len(taskids)
|
||||||
embed = discord.Embed(
|
)).format(
|
||||||
colour=discord.Colour.brand_green(),
|
tick=self.bot.config.emojis.tick,
|
||||||
description=t(_p(
|
count=len(taskids),
|
||||||
'cmd:tasklist_untick|resp:success',
|
label=tasklist.format_label(tasklist.labelid(taskids[0])) if taskids else '-'
|
||||||
"{tick} tasks marked as incomplete."
|
|
||||||
)).format(tick=self.bot.config.emojis.tick)
|
|
||||||
)
|
)
|
||||||
await ctx.interaction.edit_original_response(embed=embed)
|
)
|
||||||
|
await ctx.interaction.edit_original_response(
|
||||||
|
embed=embed,
|
||||||
|
view=None if ctx.channel.id in TasklistUI._live_[ctx.author.id] else TasklistCaller(self.bot)
|
||||||
|
)
|
||||||
|
self.bot.dispatch('tasklist_update', userid=ctx.author.id, channel=ctx.channel)
|
||||||
|
|
||||||
|
tasklist_untick_cmd.autocomplete('taskidstr')(tasks_acmpl)
|
||||||
|
|
||||||
# Setting Commands
|
# Setting Commands
|
||||||
@LionCog.placeholder_group
|
@LionCog.placeholder_group
|
||||||
|
|||||||
@@ -113,12 +113,14 @@ class TasklistSettings(SettingGroup):
|
|||||||
_display_name = _p('guildset:tasklist_channels', "tasklist_channels")
|
_display_name = _p('guildset:tasklist_channels', "tasklist_channels")
|
||||||
_desc = _p(
|
_desc = _p(
|
||||||
'guildset:tasklist_channels|desc',
|
'guildset:tasklist_channels|desc',
|
||||||
"Channels in which to allow the tasklist."
|
"Channels in which to publicly display member tasklists."
|
||||||
)
|
)
|
||||||
_long_desc = _p(
|
_long_desc = _p(
|
||||||
'guildset:tasklist_channels|long_desc',
|
'guildset:tasklist_channels|long_desc',
|
||||||
"If set, members will only be able to open their tasklist in these channels.\n"
|
"A member's tasklist (from {cmds[tasklist]}) is usually only visible to the member themselves. "
|
||||||
"If a category is selected, this will allow all channels under that category."
|
"If set, tasklists opened in `tasklist_channels` will be visible to all members, "
|
||||||
|
"and the interface will have a much longer expiry period. "
|
||||||
|
"If a category is provided, this will apply to all channels under the category."
|
||||||
)
|
)
|
||||||
_accepts = _p(
|
_accepts = _p(
|
||||||
'guildset:tasklist_channels|accepts',
|
'guildset:tasklist_channels|accepts',
|
||||||
@@ -139,12 +141,12 @@ class TasklistSettings(SettingGroup):
|
|||||||
if self.data:
|
if self.data:
|
||||||
resp = t(_p(
|
resp = t(_p(
|
||||||
'guildset:tasklist_channels|set_response|set',
|
'guildset:tasklist_channels|set_response|set',
|
||||||
"Members may now open their tasklist in the following channels: {channels}"
|
"Tasklists will now be publicly displayed in the following channels: {channels}"
|
||||||
)).format(channels=self.formatted)
|
)).format(channels=self.formatted)
|
||||||
else:
|
else:
|
||||||
resp = t(_p(
|
resp = t(_p(
|
||||||
'guildset:tasklist_channels|set_response|unset',
|
'guildset:tasklist_channels|set_response|unset',
|
||||||
"Members may now open their tasklist in any channel."
|
"Member tasklists will never be publicly displayed."
|
||||||
))
|
))
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ from meta import LionBot
|
|||||||
from meta.errors import UserInputError
|
from meta.errors import UserInputError
|
||||||
from utils.lib import utc_now
|
from utils.lib import utc_now
|
||||||
|
|
||||||
|
from . import babel
|
||||||
from .data import TasklistData
|
from .data import TasklistData
|
||||||
|
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
class Tasklist:
|
class Tasklist:
|
||||||
"""
|
"""
|
||||||
Class representing a single user's tasklist.
|
Class representing a single user's tasklist.
|
||||||
@@ -207,7 +211,7 @@ class Tasklist:
|
|||||||
"""
|
"""
|
||||||
return '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
return '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
||||||
|
|
||||||
def parse_labels(self, labelstr: str) -> Optional[list[str]]:
|
def parse_labels(self, labelstr: str) -> Optional[list[int]]:
|
||||||
"""
|
"""
|
||||||
Parse a comma separated list of labels and label ranges into a list of labels.
|
Parse a comma separated list of labels and label ranges into a list of labels.
|
||||||
|
|
||||||
@@ -239,7 +243,13 @@ class Tasklist:
|
|||||||
|
|
||||||
if len(end_label) > 1 and head != end_label[:-1]:
|
if len(end_label) > 1 and head != end_label[:-1]:
|
||||||
# Error: Parents don't match in range ...
|
# Error: Parents don't match in range ...
|
||||||
raise UserInputError("Parents don't match in range `{range}`")
|
t = self.bot.translator.t
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'tasklist|parse:multi-range|error:parents_match',
|
||||||
|
"Parents don't match in range `{range}`"
|
||||||
|
)).format(range=split)
|
||||||
|
)
|
||||||
|
|
||||||
for tail in range(max(start_tail, 1), end_tail + 1):
|
for tail in range(max(start_tail, 1), end_tail + 1):
|
||||||
label = (*head, tail)
|
label = (*head, tail)
|
||||||
@@ -255,5 +265,11 @@ class Tasklist:
|
|||||||
taskids.add(labelmap[start_label])
|
taskids.add(labelmap[start_label])
|
||||||
else:
|
else:
|
||||||
# Error
|
# Error
|
||||||
raise UserInputError("Could not parse `{range}` as a task number or range.")
|
t = self.bot.translator.t
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'tasklist|parse:multi-range|error:parse',
|
||||||
|
"Could not parse `{range}` as a task number or range."
|
||||||
|
)).format(range=split)
|
||||||
|
)
|
||||||
return list(taskids)
|
return list(taskids)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from collections import defaultdict
|
||||||
|
from enum import Enum
|
||||||
|
import asyncio
|
||||||
import re
|
import re
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ui.select import select, Select, SelectOption
|
from discord.ui.select import select, Select, SelectOption
|
||||||
@@ -24,6 +28,22 @@ checked_emoji = conf.emojis.task_checked
|
|||||||
unchecked_emoji = conf.emojis.task_unchecked
|
unchecked_emoji = conf.emojis.task_unchecked
|
||||||
|
|
||||||
|
|
||||||
|
class TasklistCaller(LeoUI):
|
||||||
|
def __init__(self, bot, **kwargs):
|
||||||
|
kwargs.setdefault('timeout', None)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.bot = bot
|
||||||
|
self.tasklist_callback.label = bot.translator.t(_p(
|
||||||
|
'ui:tasklist_caller|button:tasklist|label',
|
||||||
|
"Open Tasklist"
|
||||||
|
))
|
||||||
|
|
||||||
|
@button(label='TASKLIST_PLACEHOLDER', custom_id='open_tasklist', style=ButtonStyle.blurple)
|
||||||
|
async def tasklist_callback(self, press: discord.Interaction, pressed: Button):
|
||||||
|
cog = self.bot.get_cog('TasklistCog')
|
||||||
|
await cog.call_tasklist(press)
|
||||||
|
|
||||||
|
|
||||||
class SingleEditor(FastModal):
|
class SingleEditor(FastModal):
|
||||||
task: TextInput = TextInput(
|
task: TextInput = TextInput(
|
||||||
label='',
|
label='',
|
||||||
@@ -37,7 +57,7 @@ class SingleEditor(FastModal):
|
|||||||
|
|
||||||
parent: TextInput = TextInput(
|
parent: TextInput = TextInput(
|
||||||
label='',
|
label='',
|
||||||
max_length=10,
|
max_length=120,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,6 +116,7 @@ class BulkEditor(LeoModal):
|
|||||||
|
|
||||||
self.tasklist = tasklist
|
self.tasklist = tasklist
|
||||||
self.bot = tasklist.bot
|
self.bot = tasklist.bot
|
||||||
|
self.labelled = tasklist.labelled
|
||||||
self.userid = tasklist.userid
|
self.userid = tasklist.userid
|
||||||
|
|
||||||
self.lines = self.format_tasklist()
|
self.lines = self.format_tasklist()
|
||||||
@@ -118,7 +139,7 @@ class BulkEditor(LeoModal):
|
|||||||
"""
|
"""
|
||||||
Format the tasklist into lines of editable text.
|
Format the tasklist into lines of editable text.
|
||||||
"""
|
"""
|
||||||
labelled = self.tasklist.labelled
|
labelled = self.labelled
|
||||||
lines = {}
|
lines = {}
|
||||||
total_len = 0
|
total_len = 0
|
||||||
for label, task in labelled.items():
|
for label, task in labelled.items():
|
||||||
@@ -236,22 +257,64 @@ class BulkEditor(LeoModal):
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
class UIMode(Enum):
|
||||||
|
TOGGLE = (
|
||||||
|
_p(
|
||||||
|
'ui:tasklist|menu:main|mode:toggle|placeholder',
|
||||||
|
"Select to Toggle"
|
||||||
|
),
|
||||||
|
_p(
|
||||||
|
'ui:tasklist|menu:sub|mode:toggle|placeholder',
|
||||||
|
"Task '{label}' subtasks:"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
EDIT = (
|
||||||
|
_p(
|
||||||
|
'ui:tasklist|menu:main|mode:edit|placeholder',
|
||||||
|
"Select to Edit"
|
||||||
|
),
|
||||||
|
_p(
|
||||||
|
'ui:tasklist|menu:sub|mode:edit|placeholder',
|
||||||
|
"Task '{label}' subtasks:"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
DELETE = (
|
||||||
|
_p(
|
||||||
|
'ui:tasklist|menu:main|mode:delete|placeholder',
|
||||||
|
"Select to Delete"
|
||||||
|
),
|
||||||
|
_p(
|
||||||
|
'ui:tasklist|menu:sub|mode:delete|placeholder',
|
||||||
|
"Task '{label}' subtasks:"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def main_placeholder(self):
|
||||||
|
return self.value[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sub_placeholder(self):
|
||||||
|
return self.value[1]
|
||||||
|
|
||||||
|
|
||||||
class TasklistUI(BasePager):
|
class TasklistUI(BasePager):
|
||||||
"""
|
"""
|
||||||
Paged UI panel for managing the tasklist.
|
Paged UI panel for managing the tasklist.
|
||||||
"""
|
"""
|
||||||
# Cache of live tasklist widgets
|
# Cache of live tasklist widgets
|
||||||
# (channelid, userid) -> Tasklist
|
# userid -> channelid -> TasklistUI
|
||||||
_live_ = {}
|
_live_ = defaultdict(dict)
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
tasklist: Tasklist,
|
tasklist: Tasklist,
|
||||||
channel: discord.abc.Messageable, guild: Optional[discord.Guild] = None, **kwargs):
|
channel: discord.abc.Messageable, guild: Optional[discord.Guild] = None, **kwargs):
|
||||||
kwargs.setdefault('timeout', 3600)
|
kwargs.setdefault('timeout', 600)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.tasklist = tasklist
|
|
||||||
self.bot = tasklist.bot
|
self.bot = tasklist.bot
|
||||||
|
self.tasklist = tasklist
|
||||||
|
self.labelled = tasklist.labelled
|
||||||
self.userid = tasklist.userid
|
self.userid = tasklist.userid
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.guild = guild
|
self.guild = guild
|
||||||
@@ -263,21 +326,508 @@ class TasklistUI(BasePager):
|
|||||||
self._channelid = channel.id
|
self._channelid = channel.id
|
||||||
self.current_page = None
|
self.current_page = None
|
||||||
|
|
||||||
self._deleting = False
|
self.mode: UIMode = UIMode.TOGGLE
|
||||||
|
|
||||||
self._message: Optional[discord.Message] = None
|
self._message: Optional[discord.Message] = None
|
||||||
|
self._last_parentid: Optional[int] = None
|
||||||
|
self._subtree_root: Optional[int] = None
|
||||||
|
|
||||||
self.button_labels()
|
|
||||||
self.set_active()
|
self.set_active()
|
||||||
|
|
||||||
|
# ----- UI API -----
|
||||||
@classmethod
|
@classmethod
|
||||||
async def fetch(cls, tasklist, channel, *args, **kwargs):
|
def fetch(cls, tasklist, channel, *args, **kwargs):
|
||||||
key = (channel.id, tasklist.userid)
|
userid = tasklist.userid
|
||||||
if key not in cls._live_:
|
channelid = channel.id
|
||||||
|
if channelid not in cls._live_[userid]:
|
||||||
self = cls(tasklist, channel, *args, **kwargs)
|
self = cls(tasklist, channel, *args, **kwargs)
|
||||||
cls._live_[key] = self
|
cls._live_[userid][channelid] = self
|
||||||
return cls._live_[key]
|
return cls._live_[userid][channelid]
|
||||||
|
|
||||||
|
async def run(self, interaction: discord.Interaction):
|
||||||
|
await self.refresh()
|
||||||
|
await self.redraw(interaction)
|
||||||
|
|
||||||
|
async def summon(self, force=False):
|
||||||
|
"""
|
||||||
|
Delete, refresh, and redisplay the tasklist widget as a non-ephemeral message in the current channel.
|
||||||
|
|
||||||
|
May raise `discord.HTTPException` (from `redraw`) if something goes wrong with the send.
|
||||||
|
"""
|
||||||
|
await self.refresh()
|
||||||
|
|
||||||
|
resend = force or not await self._check_recent()
|
||||||
|
if resend and self._message:
|
||||||
|
# Delete our current message if possible
|
||||||
|
try:
|
||||||
|
await self._message.delete()
|
||||||
|
except discord.HTTPException:
|
||||||
|
# If we cannot delete, it has probably already been deleted
|
||||||
|
# Or we don't have permission somehow
|
||||||
|
pass
|
||||||
|
self._message = None
|
||||||
|
|
||||||
|
# Redraw
|
||||||
|
try:
|
||||||
|
await self.redraw()
|
||||||
|
except discord.HTTPException:
|
||||||
|
if self._message:
|
||||||
|
self._message = None
|
||||||
|
await self.redraw()
|
||||||
|
|
||||||
|
async def page_cmd(self, interaction: discord.Interaction, value: str):
|
||||||
|
return await Pager.page_cmd(self, interaction, value)
|
||||||
|
|
||||||
|
async def page_acmpl(self, interaction: discord.Interaction, partial: str):
|
||||||
|
return await Pager.page_acmpl(self, interaction, partial)
|
||||||
|
|
||||||
|
# ----- Utilities / Workers ------
|
||||||
|
async def _check_recent(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check whether the tasklist message is a "recent" message in the channel.
|
||||||
|
"""
|
||||||
|
if self._message is not None:
|
||||||
|
height = 0
|
||||||
|
async for message in self.channel.history(limit=5):
|
||||||
|
if message.id == self._message.id:
|
||||||
|
return True
|
||||||
|
if message.id < self._message.id:
|
||||||
|
return False
|
||||||
|
if message.attachments or message.embeds or height > 20:
|
||||||
|
return False
|
||||||
|
height += message.content.count('\n')
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _format_page(self, page: list[tuple[tuple[int, ...], TasklistData.Task]]) -> str:
|
||||||
|
"""
|
||||||
|
Format a single block of page data into the task codeblock.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
numpad = max(sum(len(str(counter)) - 1 for counter in label) for label, _ in page)
|
||||||
|
for label, task in page:
|
||||||
|
label_string = '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
||||||
|
number = f"**`{label_string}`**"
|
||||||
|
if len(label) > 1:
|
||||||
|
depth = sum(len(str(c)) + 1 for c in label[:-1]) * ' '
|
||||||
|
depth = f"`{depth}`"
|
||||||
|
else:
|
||||||
|
depth = ''
|
||||||
|
task_string = "{depth}{cross}{number} {content}{cross}".format(
|
||||||
|
depth=depth,
|
||||||
|
number=number,
|
||||||
|
emoji=unchecked_emoji if task.completed_at is None else checked_emoji,
|
||||||
|
content=task.content,
|
||||||
|
cross='~~' if task.completed_at is not None else ''
|
||||||
|
)
|
||||||
|
lines.append(task_string)
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def _format_page_text(self, page: list[tuple[tuple[int, ...], TasklistData.Task]]) -> str:
|
||||||
|
"""
|
||||||
|
Format a single block of page data into the task codeblock.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
numpad = max(sum(len(str(counter)) - 1 for counter in label) for label, _ in page)
|
||||||
|
for label, task in page:
|
||||||
|
box = '[ ]' if task.completed_at is None else f"[{checkmark}]"
|
||||||
|
task_string = "{prepad} {depth} {content}".format(
|
||||||
|
prepad=' ' * numpad,
|
||||||
|
depth=(len(label) - 1) * ' ',
|
||||||
|
content=task.content
|
||||||
|
)
|
||||||
|
label_string = '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
||||||
|
taskline = box + ' ' + label_string + task_string[len(label_string):]
|
||||||
|
lines.append(taskline)
|
||||||
|
return "```md\n{}```".format('\n'.join(lines))
|
||||||
|
|
||||||
|
def _format_options(self, task_block) -> list[SelectOption]:
|
||||||
|
options = []
|
||||||
|
for lbl, task in task_block:
|
||||||
|
value = str(task.taskid)
|
||||||
|
lblstr = '.'.join(map(str, lbl)) + '.' * (len(lbl) == 1)
|
||||||
|
name = f"{lblstr} {task.content[:100 - len(lblstr) - 1]}"
|
||||||
|
emoji = unchecked_emoji if task.completed_at is None else checked_emoji
|
||||||
|
options.append(SelectOption(label=name, value=value, emoji=emoji))
|
||||||
|
return options
|
||||||
|
|
||||||
|
def _format_parent(self, parentid) -> str:
|
||||||
|
parentstr = ''
|
||||||
|
if parentid is not None:
|
||||||
|
task = self.tasklist.tasklist.get(parentid, None)
|
||||||
|
if task:
|
||||||
|
parent_label = self.tasklist.format_label(self.tasklist.labelid(parentid)).strip('.')
|
||||||
|
parentstr = f"{parent_label}: {task.content}"
|
||||||
|
return parentstr
|
||||||
|
|
||||||
|
def _parse_parent(self, provided: str) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Parse a provided parent field.
|
||||||
|
|
||||||
|
May raise UserInputError if parsing fails.
|
||||||
|
"""
|
||||||
|
t = self.bot.translator.t
|
||||||
|
provided = provided.strip()
|
||||||
|
|
||||||
|
if provided.split(':', maxsplit=1)[0].replace('.', '').strip().isdigit():
|
||||||
|
# Assume task label
|
||||||
|
label, _, _ = provided.partition(':')
|
||||||
|
label = label.strip()
|
||||||
|
pid = self.tasklist.parse_label(label)
|
||||||
|
if pid is None:
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'ui:tasklist_single_editor|field:parent|error:parse_id',
|
||||||
|
"Could not find the given parent task number `{input}` in your tasklist."
|
||||||
|
)).format(input=label)
|
||||||
|
)
|
||||||
|
elif provided:
|
||||||
|
# Search for matching tasks
|
||||||
|
matching = [
|
||||||
|
task.taskid
|
||||||
|
for task in self.tasklist.tasklist.values()
|
||||||
|
if provided.lower() in task.content.lower()
|
||||||
|
]
|
||||||
|
if len(matching) > 1:
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'ui:tasklist_single_editor|field:parent|error:multiple_matching',
|
||||||
|
"Multiple tasks matching given parent task `{input}`. Please use a task number instead!"
|
||||||
|
)).format(input=provided)
|
||||||
|
)
|
||||||
|
elif not matching:
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'ui:tasklist_single_editor|field:parent|error:no_matching',
|
||||||
|
"No tasks matching given parent task `{input}`."
|
||||||
|
)).format(input=provided)
|
||||||
|
)
|
||||||
|
pid = matching[0]
|
||||||
|
else:
|
||||||
|
pid = None
|
||||||
|
|
||||||
|
return pid
|
||||||
|
|
||||||
|
# ----- Components -----
|
||||||
|
async def _toggle_menu(self, interaction: discord.Interaction, selected: Select, subtree: bool):
|
||||||
|
await interaction.response.defer()
|
||||||
|
taskids = list(map(int, selected.values))
|
||||||
|
tasks = await self.tasklist.fetch_tasks(*taskids)
|
||||||
|
to_complete = [task for task in tasks if task.completed_at is None]
|
||||||
|
to_uncomplete = [task for task in tasks if task.completed_at is not None]
|
||||||
|
if to_complete:
|
||||||
|
await self.tasklist.update_tasks(
|
||||||
|
*(t.taskid for t in to_complete),
|
||||||
|
cascade=True,
|
||||||
|
completed_at=utc_now()
|
||||||
|
)
|
||||||
|
if self.guild:
|
||||||
|
if (member := self.guild.get_member(self.userid)):
|
||||||
|
self.bot.dispatch('tasks_completed', member, *(t.taskid for t in to_complete))
|
||||||
|
if to_uncomplete:
|
||||||
|
await self.tasklist.update_tasks(
|
||||||
|
*(t.taskid for t in to_uncomplete),
|
||||||
|
completed_at=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the selected tasks share a parent, and we are not in the subtree menu, change the subtree root
|
||||||
|
if taskids and not subtree:
|
||||||
|
labelled = self.labelled
|
||||||
|
mapper = {t.taskid: label for label, t in labelled.items()}
|
||||||
|
shared_root = None
|
||||||
|
for task in tasks:
|
||||||
|
pid = task.parentid
|
||||||
|
plabel = mapper[pid] if pid else ()
|
||||||
|
if shared_root:
|
||||||
|
shared_root = tuple(i for i, j in zip(shared_root, plabel) if i == j)
|
||||||
|
else:
|
||||||
|
shared_root = plabel
|
||||||
|
if not shared_root:
|
||||||
|
break
|
||||||
|
if shared_root:
|
||||||
|
self._subtree_root = labelled[shared_root].taskid
|
||||||
|
|
||||||
|
self.bot.dispatch('tasklist_update', userid=self.userid, channel=self.channel, summon=False)
|
||||||
|
|
||||||
|
async def _delete_menu(self, interaction: discord.Interaction, selected: Select, subtree: bool):
|
||||||
|
await interaction.response.defer()
|
||||||
|
taskids = list(map(int, selected.values))
|
||||||
|
if taskids:
|
||||||
|
await self.tasklist.update_tasks(
|
||||||
|
*taskids,
|
||||||
|
cascade=True,
|
||||||
|
deleted_at=utc_now()
|
||||||
|
)
|
||||||
|
self.bot.dispatch('tasklist_update', userid=self.userid, channel=self.channel, summon=False)
|
||||||
|
|
||||||
|
async def _edit_menu(self, interaction: discord.Interaction, selected: Select, subtree: bool):
|
||||||
|
if not selected.values:
|
||||||
|
await interaction.response.defer()
|
||||||
|
else:
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
taskid = int(selected.values[0])
|
||||||
|
task = self.tasklist.tasklist[taskid]
|
||||||
|
|
||||||
|
editor = SingleEditor(
|
||||||
|
title=t(_p('ui:tasklist|menu:edit|modal:title', "Edit task"))
|
||||||
|
)
|
||||||
|
editor.parent.default = self._format_parent(task.parentid)
|
||||||
|
editor.task.default = task.content
|
||||||
|
|
||||||
|
@editor.submit_callback()
|
||||||
|
async def create_task(interaction):
|
||||||
|
new_task = editor.task.value
|
||||||
|
new_parentid = self._parse_parent(editor.parent.value)
|
||||||
|
await interaction.response.defer()
|
||||||
|
if task.content != new_task or task.parentid != new_parentid:
|
||||||
|
await task.update(content=new_task, parentid=new_parentid)
|
||||||
|
self._last_parentid = new_parentid
|
||||||
|
if not subtree:
|
||||||
|
self._subtree_root = new_parentid
|
||||||
|
self.bot.dispatch('tasklist_update', userid=self.userid, channel=self.channel, summon=False)
|
||||||
|
|
||||||
|
await interaction.response.send_modal(editor)
|
||||||
|
|
||||||
|
@select(placeholder="MAIN_MENU_PLACEHOLDER")
|
||||||
|
async def main_menu(self, interaction: discord.Interaction, selected: Select):
|
||||||
|
if self.mode is UIMode.TOGGLE:
|
||||||
|
await self._toggle_menu(interaction, selected, False)
|
||||||
|
elif self.mode is UIMode.DELETE:
|
||||||
|
await self._delete_menu(interaction, selected, False)
|
||||||
|
elif self.mode is UIMode.EDIT:
|
||||||
|
await self._edit_menu(interaction, selected, False)
|
||||||
|
|
||||||
|
async def main_menu_refresh(self):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
menu = self.main_menu
|
||||||
|
menu.placeholder = t(self.mode.main_placeholder)
|
||||||
|
|
||||||
|
block = self._pages[self.page_num % len(self._pages)]
|
||||||
|
options = self._format_options(block)
|
||||||
|
|
||||||
|
menu.options = options
|
||||||
|
menu.min_values = 0
|
||||||
|
menu.max_values = len(options) if self.mode is not UIMode.EDIT else 1
|
||||||
|
|
||||||
|
@select(placeholder="SUB_MENU_PLACEHOLDER")
|
||||||
|
async def sub_menu(self, interaction: discord.Interaction, selected: Select):
|
||||||
|
if self.mode is UIMode.TOGGLE:
|
||||||
|
await self._toggle_menu(interaction, selected, True)
|
||||||
|
elif self.mode is UIMode.DELETE:
|
||||||
|
await self._delete_menu(interaction, selected, True)
|
||||||
|
elif self.mode is UIMode.EDIT:
|
||||||
|
await self._edit_menu(interaction, selected, True)
|
||||||
|
|
||||||
|
async def sub_menu_refresh(self):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
menu = self.sub_menu
|
||||||
|
|
||||||
|
options = []
|
||||||
|
if self._subtree_root:
|
||||||
|
labelled = self.labelled
|
||||||
|
mapper = {t.taskid: label for label, t in labelled.items()}
|
||||||
|
rootid = self._subtree_root
|
||||||
|
rootlabel = mapper.get(rootid, ())
|
||||||
|
if rootlabel:
|
||||||
|
menu.placeholder = t(self.mode.sub_placeholder).format(
|
||||||
|
label=self.tasklist.format_label(rootlabel).strip('.'),
|
||||||
|
)
|
||||||
|
children = {
|
||||||
|
label: taskid
|
||||||
|
for label, taskid in labelled.items()
|
||||||
|
if all(i == j for i, j in zip(label, rootlabel))
|
||||||
|
}
|
||||||
|
this_page = self._pages[self.page_num % len(self._pages)]
|
||||||
|
if len(children) <= 25:
|
||||||
|
# Show all the children even if they don't display on the page
|
||||||
|
block = list(children.items())
|
||||||
|
else:
|
||||||
|
# Only show the children which display
|
||||||
|
page_children = [
|
||||||
|
(label, tid) for label, tid in this_page if label in children and tid != rootid
|
||||||
|
][:24]
|
||||||
|
if page_children:
|
||||||
|
block = [(rootlabel, rootid), *page_children]
|
||||||
|
else:
|
||||||
|
block = []
|
||||||
|
# Special case if the subtree is exactly the same as the page
|
||||||
|
if not (len(block) == len(this_page) and all(i[0] == j[0] for i, j in zip(block, this_page))):
|
||||||
|
options = self._format_options(block)
|
||||||
|
|
||||||
|
menu.options = options
|
||||||
|
menu.min_values = 0
|
||||||
|
menu.max_values = len(options) if self.mode is not UIMode.EDIT else 1
|
||||||
|
|
||||||
|
@button(label='NEW_BUTTON_PLACEHOLDER', style=ButtonStyle.green, emoji=conf.emojis.task_new)
|
||||||
|
async def new_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
editor = SingleEditor(
|
||||||
|
title=t(_p('ui:tasklist_single_editor|title', "Add task"))
|
||||||
|
)
|
||||||
|
editor.parent.default = self._format_parent(self._last_parentid)
|
||||||
|
|
||||||
|
@editor.submit_callback()
|
||||||
|
async def create_task(interaction):
|
||||||
|
new_task = editor.task.value
|
||||||
|
parent = editor.parent.value
|
||||||
|
pid = self._parse_parent(parent)
|
||||||
|
self._last_parentid = pid
|
||||||
|
self._subtree_root = pid
|
||||||
|
await interaction.response.defer()
|
||||||
|
await self.tasklist.create_task(new_task, parentid=pid)
|
||||||
|
self.bot.dispatch('tasklist_update', userid=self.userid, channel=self.channel, summon=False)
|
||||||
|
|
||||||
|
await press.response.send_modal(editor)
|
||||||
|
|
||||||
|
async def new_button_refresh(self):
|
||||||
|
self.new_button.label = ""
|
||||||
|
|
||||||
|
@button(label="EDIT_MODE_PLACEHOLDER", style=ButtonStyle.blurple)
|
||||||
|
async def edit_mode_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer()
|
||||||
|
self.mode = UIMode.EDIT
|
||||||
|
await self.redraw()
|
||||||
|
|
||||||
|
async def edit_mode_button_refresh(self):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button = self.edit_mode_button
|
||||||
|
|
||||||
|
button.style = ButtonStyle.blurple if (self.mode is UIMode.EDIT) else ButtonStyle.grey
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:tasklist|button:edit_mode|label',
|
||||||
|
"Edit"
|
||||||
|
))
|
||||||
|
|
||||||
|
@button(label="DELETE_MODE_PLACEHOLDER", style=ButtonStyle.blurple)
|
||||||
|
async def delete_mode_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer()
|
||||||
|
self.mode = UIMode.DELETE
|
||||||
|
await self.redraw()
|
||||||
|
|
||||||
|
async def delete_mode_button_refresh(self):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button = self.delete_mode_button
|
||||||
|
|
||||||
|
button.style = ButtonStyle.blurple if (self.mode is UIMode.DELETE) else ButtonStyle.grey
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:tasklist|button:delete_mode|label',
|
||||||
|
"Delete"
|
||||||
|
))
|
||||||
|
|
||||||
|
@button(label="TOGGLE_MODE_PLACEHOLDER", style=ButtonStyle.blurple)
|
||||||
|
async def toggle_mode_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer()
|
||||||
|
self.mode = UIMode.TOGGLE
|
||||||
|
await self.redraw()
|
||||||
|
|
||||||
|
async def toggle_mode_button_refresh(self):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button = self.toggle_mode_button
|
||||||
|
|
||||||
|
button.style = ButtonStyle.blurple if (self.mode is UIMode.TOGGLE) else ButtonStyle.grey
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:tasklist|button:toggle_mode|label',
|
||||||
|
"Toggle"
|
||||||
|
))
|
||||||
|
|
||||||
|
@button(label="EDIT_BULK_PLACEHOLDER", style=ButtonStyle.blurple)
|
||||||
|
async def edit_bulk_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
editor = BulkEditor(self.tasklist)
|
||||||
|
|
||||||
|
@editor.add_callback
|
||||||
|
async def editor_callback(interaction: discord.Interaction):
|
||||||
|
self.bot.dispatch('tasklist_update', userid=self.userid, channel=self.channel, summon=False)
|
||||||
|
|
||||||
|
await press.response.send_modal(editor)
|
||||||
|
|
||||||
|
async def edit_bulk_button_refresh(self):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
button = self.edit_bulk_button
|
||||||
|
button.disabled = (len(self.labelled) == 0)
|
||||||
|
button.label = t(_p(
|
||||||
|
'ui:tasklist|button:edit_bulk|label',
|
||||||
|
"Bulk Edit"
|
||||||
|
))
|
||||||
|
|
||||||
|
@button(label='CLEAR_PLACEHOLDER', style=ButtonStyle.red)
|
||||||
|
async def clear_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer()
|
||||||
|
await self.tasklist.update_tasklist(
|
||||||
|
deleted_at=utc_now(),
|
||||||
|
)
|
||||||
|
self.bot.dispatch('tasklist_update', userid=self.userid, channel=self.channel, summon=False)
|
||||||
|
|
||||||
|
async def clear_button_refresh(self):
|
||||||
|
self.clear_button.label = self.bot.translator.t(_p(
|
||||||
|
'ui:tasklist|button:clear|label', "Clear Tasklist"
|
||||||
|
))
|
||||||
|
self.clear_button.disabled = (len(self.labelled) == 0)
|
||||||
|
|
||||||
|
@button(label="SAVE_PLACEHOLDER", style=ButtonStyle.grey, emoji=conf.emojis.task_save)
|
||||||
|
async def save_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
"""
|
||||||
|
Send the tasklist to the user as a markdown file.
|
||||||
|
"""
|
||||||
|
t = self.bot.translator.t
|
||||||
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
|
# Build the tasklist file
|
||||||
|
# Lazy way of getting the tasklist
|
||||||
|
contents = BulkEditor(self.tasklist).tasklist_editor.default
|
||||||
|
with StringIO(contents) as fp:
|
||||||
|
fp.seek(0)
|
||||||
|
file = discord.File(fp, filename='tasklist.md')
|
||||||
|
contents = t(_p(
|
||||||
|
'ui:tasklist|button:save|dm:contents',
|
||||||
|
"Your tasklist as of {now} is attached. Click here to jump back: {jump}"
|
||||||
|
)).format(
|
||||||
|
now=discord.utils.format_dt(utc_now()),
|
||||||
|
jump=press.message.jump_url
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await press.user.send(contents, file=file, silent=True)
|
||||||
|
except discord.HTTPClient:
|
||||||
|
fp.seek(0)
|
||||||
|
file = discord.File(fp, filename='tasklist.md')
|
||||||
|
await press.followup.send(
|
||||||
|
t(_p(
|
||||||
|
'ui:tasklist|button:save|error:dms',
|
||||||
|
"Could not DM you! Do you have me blocked? Tasklist attached below."
|
||||||
|
)),
|
||||||
|
file=file
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fp.seek(0)
|
||||||
|
file = discord.File(fp, filename='tasklist.md')
|
||||||
|
await press.followup.send(file=file)
|
||||||
|
|
||||||
|
async def save_button_refresh(self):
|
||||||
|
self.save_button.disabled = (len(self.labelled) == 0)
|
||||||
|
self.save_button.label = ''
|
||||||
|
|
||||||
|
@button(label="REFRESH_PLACEHOLDER", style=ButtonStyle.grey, emoji=conf.emojis.refresh, custom_id='open_tasklist')
|
||||||
|
async def refresh_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer()
|
||||||
|
await self.refresh()
|
||||||
|
await self.redraw()
|
||||||
|
|
||||||
|
async def refresh_button_refresh(self):
|
||||||
|
self.refresh_button.label = ''
|
||||||
|
|
||||||
|
@button(label="QUIT_PLACEHOLDER", style=ButtonStyle.grey, emoji=conf.emojis.cancel)
|
||||||
|
async def quit_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer()
|
||||||
|
if self._message is not None:
|
||||||
|
try:
|
||||||
|
await self._message.delete()
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
await self.close()
|
||||||
|
|
||||||
|
async def quit_button_refresh(self):
|
||||||
|
self.quit_button.label = ''
|
||||||
|
|
||||||
|
# ----- UI Flow -----
|
||||||
def access_check(self, userid):
|
def access_check(self, userid):
|
||||||
return userid == self.userid
|
return userid == self.userid
|
||||||
|
|
||||||
@@ -298,7 +848,7 @@ class TasklistUI(BasePager):
|
|||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
self.set_inactive()
|
self.set_inactive()
|
||||||
self._live_.pop((self.channel.id, self.userid), None)
|
self._live_[self.userid].pop(self.channel.id, None)
|
||||||
|
|
||||||
if self._message is not None:
|
if self._message is not None:
|
||||||
try:
|
try:
|
||||||
@@ -313,48 +863,6 @@ class TasklistUI(BasePager):
|
|||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def summon(self):
|
|
||||||
"""
|
|
||||||
Refresh and re-display the tasklist widget as required.
|
|
||||||
"""
|
|
||||||
await self.refresh()
|
|
||||||
|
|
||||||
resend = not await self._check_recent()
|
|
||||||
if resend and self._message:
|
|
||||||
# Delete our current message if possible
|
|
||||||
try:
|
|
||||||
await self._message.delete()
|
|
||||||
except discord.HTTPException:
|
|
||||||
# If we cannot delete, it has probably already been deleted
|
|
||||||
# Or we don't have permission somehow
|
|
||||||
pass
|
|
||||||
self._message = None
|
|
||||||
|
|
||||||
# Redraw
|
|
||||||
try:
|
|
||||||
await self.redraw()
|
|
||||||
except discord.HTTPException:
|
|
||||||
if self._message:
|
|
||||||
self._message = None
|
|
||||||
await self.redraw()
|
|
||||||
|
|
||||||
async def _check_recent(self) -> bool:
|
|
||||||
"""
|
|
||||||
Check whether the tasklist message is a "recent" message in the channel.
|
|
||||||
"""
|
|
||||||
if self._message is not None:
|
|
||||||
height = 0
|
|
||||||
async for message in self.channel.history(limit=5):
|
|
||||||
if message.id == self._message.id:
|
|
||||||
return True
|
|
||||||
if message.id < self._message.id:
|
|
||||||
return False
|
|
||||||
if message.attachments or message.embeds or height > 20:
|
|
||||||
return False
|
|
||||||
height += message.content.count('\n')
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def get_page(self, page_id) -> MessageArgs:
|
async def get_page(self, page_id) -> MessageArgs:
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
|
|
||||||
@@ -400,56 +908,8 @@ class TasklistUI(BasePager):
|
|||||||
page_args = MessageArgs(embed=embed)
|
page_args = MessageArgs(embed=embed)
|
||||||
return page_args
|
return page_args
|
||||||
|
|
||||||
async def page_cmd(self, interaction: discord.Interaction, value: str):
|
|
||||||
return await Pager.page_cmd(self, interaction, value)
|
|
||||||
|
|
||||||
async def page_acmpl(self, interaction: discord.Interaction, partial: str):
|
|
||||||
return await Pager.page_acmpl(self, interaction, partial)
|
|
||||||
|
|
||||||
def _format_page(self, page: list[tuple[tuple[int, ...], TasklistData.Task]]) -> str:
|
|
||||||
"""
|
|
||||||
Format a single block of page data into the task codeblock.
|
|
||||||
"""
|
|
||||||
lines = []
|
|
||||||
numpad = max(sum(len(str(counter)) - 1 for counter in label) for label, _ in page)
|
|
||||||
for label, task in page:
|
|
||||||
label_string = '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
|
||||||
number = f"**`{label_string}`**"
|
|
||||||
if len(label) > 1:
|
|
||||||
depth = sum(len(str(c)) + 1 for c in label[:-1]) * ' '
|
|
||||||
depth = f"`{depth}`"
|
|
||||||
else:
|
|
||||||
depth = ''
|
|
||||||
task_string = "{depth}{cross}{number} {content}{cross}".format(
|
|
||||||
depth=depth,
|
|
||||||
number=number,
|
|
||||||
emoji=unchecked_emoji if task.completed_at is None else checked_emoji,
|
|
||||||
content=task.content,
|
|
||||||
cross='~~' if task.completed_at is not None else ''
|
|
||||||
)
|
|
||||||
lines.append(task_string)
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def _format_page_text(self, page: list[tuple[tuple[int, ...], TasklistData.Task]]) -> str:
|
|
||||||
"""
|
|
||||||
Format a single block of page data into the task codeblock.
|
|
||||||
"""
|
|
||||||
lines = []
|
|
||||||
numpad = max(sum(len(str(counter)) - 1 for counter in label) for label, _ in page)
|
|
||||||
for label, task in page:
|
|
||||||
box = '[ ]' if task.completed_at is None else f"[{checkmark}]"
|
|
||||||
task_string = "{prepad} {depth} {content}".format(
|
|
||||||
prepad=' ' * numpad,
|
|
||||||
depth=(len(label) - 1) * ' ',
|
|
||||||
content=task.content
|
|
||||||
)
|
|
||||||
label_string = '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
|
||||||
taskline = box + ' ' + label_string + task_string[len(label_string):]
|
|
||||||
lines.append(taskline)
|
|
||||||
return "```md\n{}```".format('\n'.join(lines))
|
|
||||||
|
|
||||||
def refresh_pages(self):
|
def refresh_pages(self):
|
||||||
labelled = list(self.tasklist.labelled.items())
|
labelled = list(self.labelled.items())
|
||||||
count = len(labelled)
|
count = len(labelled)
|
||||||
pages = []
|
pages = []
|
||||||
|
|
||||||
@@ -478,181 +938,76 @@ class TasklistUI(BasePager):
|
|||||||
self._pages = pages
|
self._pages = pages
|
||||||
return pages
|
return pages
|
||||||
|
|
||||||
@select(placeholder="TOGGLE_PLACEHOLDER")
|
|
||||||
async def toggle_selector(self, interaction: discord.Interaction, selected: Select):
|
|
||||||
await interaction.response.defer()
|
|
||||||
taskids = list(map(int, selected.values))
|
|
||||||
tasks = await self.tasklist.fetch_tasks(*taskids)
|
|
||||||
to_complete = [task for task in tasks if task.completed_at is None]
|
|
||||||
to_uncomplete = [task for task in tasks if task.completed_at is not None]
|
|
||||||
if to_complete:
|
|
||||||
await self.tasklist.update_tasks(
|
|
||||||
*(t.taskid for t in to_complete),
|
|
||||||
cascade=True,
|
|
||||||
completed_at=utc_now()
|
|
||||||
)
|
|
||||||
if self.guild:
|
|
||||||
if (member := self.guild.get_member(self.userid)):
|
|
||||||
self.bot.dispatch('tasks_completed', member, *(t.taskid for t in to_complete))
|
|
||||||
if to_uncomplete:
|
|
||||||
await self.tasklist.update_tasks(
|
|
||||||
*(t.taskid for t in to_uncomplete),
|
|
||||||
completed_at=None
|
|
||||||
)
|
|
||||||
await self.refresh()
|
|
||||||
await self.redraw()
|
|
||||||
|
|
||||||
async def toggle_selector_refresh(self):
|
|
||||||
t = self.bot.translator.t
|
|
||||||
self.toggle_selector.placeholder = t(_p(
|
|
||||||
'ui:tasklist|menu:toggle_selector|placeholder',
|
|
||||||
"Select to Toggle"
|
|
||||||
))
|
|
||||||
options = []
|
|
||||||
block = self._pages[self.page_num % len(self._pages)]
|
|
||||||
colwidth = max(sum(len(str(c)) + 1 for c in lbl) for lbl, _ in block)
|
|
||||||
for lbl, task in block:
|
|
||||||
value = str(task.taskid)
|
|
||||||
lblstr = '.'.join(map(str, lbl)) + '.' * (len(lbl) == 1)
|
|
||||||
name = f"{lblstr:<{colwidth}} {task.content}"
|
|
||||||
emoji = unchecked_emoji if task.completed_at is None else checked_emoji
|
|
||||||
options.append(SelectOption(label=name, value=value, emoji=emoji))
|
|
||||||
|
|
||||||
self.toggle_selector.options = options
|
|
||||||
self.toggle_selector.min_values = 0
|
|
||||||
self.toggle_selector.max_values = len(options)
|
|
||||||
|
|
||||||
@button(label="NEW_PLACEHOLDER", style=ButtonStyle.green)
|
|
||||||
async def new_pressed(self, interaction: discord.Interaction, pressed: Button):
|
|
||||||
t = self.bot.translator.t
|
|
||||||
editor = SingleEditor(
|
|
||||||
title=t(_p('ui:tasklist_single_editor|title', "Add task"))
|
|
||||||
)
|
|
||||||
|
|
||||||
@editor.submit_callback()
|
|
||||||
async def create_task(interaction):
|
|
||||||
new_task = editor.task.value
|
|
||||||
parent = editor.parent.value
|
|
||||||
pid = self.tasklist.parse_label(parent) if parent else None
|
|
||||||
if parent and pid is None:
|
|
||||||
# Could not parse
|
|
||||||
raise UserInputError(
|
|
||||||
t(_p(
|
|
||||||
'ui:tasklist_single_editor|error:parse_parent',
|
|
||||||
"Could not find the given parent task number `{input}` in your tasklist."
|
|
||||||
)).format(input=parent)
|
|
||||||
)
|
|
||||||
await interaction.response.defer()
|
|
||||||
await self.tasklist.create_task(new_task, parentid=pid)
|
|
||||||
await self.refresh()
|
|
||||||
await self.redraw()
|
|
||||||
|
|
||||||
await interaction.response.send_modal(editor)
|
|
||||||
|
|
||||||
@button(label="EDITOR_PLACEHOLDER", style=ButtonStyle.blurple)
|
|
||||||
async def edit_pressed(self, interaction: discord.Interaction, pressed: Button):
|
|
||||||
editor = BulkEditor(self.tasklist)
|
|
||||||
|
|
||||||
@editor.add_callback
|
|
||||||
async def editor_callback(interaction: discord.Interaction):
|
|
||||||
await self.refresh()
|
|
||||||
await self.redraw()
|
|
||||||
|
|
||||||
await interaction.response.send_modal(editor)
|
|
||||||
|
|
||||||
@button(label="DELETE_PLACEHOLDER", style=ButtonStyle.red)
|
|
||||||
async def del_pressed(self, interaction: discord.Interaction, pressed: Button):
|
|
||||||
self._deleting = 1 - self._deleting
|
|
||||||
await interaction.response.defer()
|
|
||||||
await self.refresh()
|
|
||||||
await self.redraw()
|
|
||||||
|
|
||||||
@select(placeholder="DELETE_SELECT_PLACEHOLDER")
|
|
||||||
async def delete_selector(self, interaction: discord.Interaction, selected: Select):
|
|
||||||
await interaction.response.defer()
|
|
||||||
taskids = list(map(int, selected.values))
|
|
||||||
if taskids:
|
|
||||||
await self.tasklist.update_tasks(
|
|
||||||
*taskids,
|
|
||||||
cascade=True,
|
|
||||||
deleted_at=utc_now()
|
|
||||||
)
|
|
||||||
await self.refresh()
|
|
||||||
await self.redraw()
|
|
||||||
|
|
||||||
async def delete_selector_refresh(self):
|
|
||||||
t = self.bot.translator.t
|
|
||||||
self.delete_selector.placeholder = t(_p('ui:tasklist|menu:delete|placeholder', "Select to Delete"))
|
|
||||||
self.delete_selector.options = self.toggle_selector.options
|
|
||||||
self.delete_selector.max_values = len(self.toggle_selector.options)
|
|
||||||
|
|
||||||
@button(label="ClOSE_PLACEHOLDER", style=ButtonStyle.red)
|
|
||||||
async def close_pressed(self, interaction: discord.Interaction, pressed: Button):
|
|
||||||
await interaction.response.defer()
|
|
||||||
if self._message is not None:
|
|
||||||
try:
|
|
||||||
await self._message.delete()
|
|
||||||
except discord.HTTPException:
|
|
||||||
pass
|
|
||||||
await self.close()
|
|
||||||
|
|
||||||
@button(label="CLEAR_PLACEHOLDER", style=ButtonStyle.red)
|
|
||||||
async def clear_pressed(self, interaction: discord.Interaction, pressed: Button):
|
|
||||||
await interaction.response.defer()
|
|
||||||
await self.tasklist.update_tasklist(
|
|
||||||
deleted_at=utc_now(),
|
|
||||||
)
|
|
||||||
await self.refresh()
|
|
||||||
await self.redraw()
|
|
||||||
|
|
||||||
def button_labels(self):
|
|
||||||
t = self.bot.translator.t
|
|
||||||
self.new_pressed.label = t(_p('ui:tasklist|button:new', "New"))
|
|
||||||
self.edit_pressed.label = t(_p('ui:tasklist|button:edit', "Edit"))
|
|
||||||
self.del_pressed.label = t(_p('ui:tasklist|button:delete', "Delete"))
|
|
||||||
self.clear_pressed.label = t(_p('ui:tasklist|button:clear', "Clear"))
|
|
||||||
self.close_pressed.label = t(_p('ui:tasklist|button:close', "Close"))
|
|
||||||
|
|
||||||
async def refresh(self):
|
async def refresh(self):
|
||||||
# Refresh data
|
# Refresh data
|
||||||
await self.tasklist.refresh()
|
await self.tasklist.refresh()
|
||||||
|
self.labelled = self.tasklist.labelled
|
||||||
self.refresh_pages()
|
self.refresh_pages()
|
||||||
|
|
||||||
async def redraw(self):
|
async def refresh_components(self):
|
||||||
self.current_page = await self.get_page(self.page_num)
|
await asyncio.gather(
|
||||||
|
self.main_menu_refresh(),
|
||||||
|
self.sub_menu_refresh(),
|
||||||
|
self.new_button_refresh(),
|
||||||
|
self.edit_mode_button_refresh(),
|
||||||
|
self.delete_mode_button_refresh(),
|
||||||
|
self.toggle_mode_button_refresh(),
|
||||||
|
self.edit_bulk_button_refresh(),
|
||||||
|
self.clear_button_refresh(),
|
||||||
|
self.save_button_refresh(),
|
||||||
|
self.refresh_button_refresh(),
|
||||||
|
self.quit_button_refresh(),
|
||||||
|
)
|
||||||
|
|
||||||
|
action_row = [
|
||||||
|
self.new_button, self.toggle_mode_button, self.edit_mode_button, self.delete_mode_button,
|
||||||
|
]
|
||||||
|
if self.mode is UIMode.EDIT:
|
||||||
|
action_row.append(self.edit_bulk_button)
|
||||||
|
elif self.mode is UIMode.DELETE:
|
||||||
|
action_row.append(self.clear_button)
|
||||||
|
|
||||||
|
main_row = (self.main_menu,) if self.main_menu.options else ()
|
||||||
|
sub_row = (self.sub_menu,) if self.sub_menu.options else ()
|
||||||
|
|
||||||
# Refresh the layout
|
|
||||||
if len(self._pages) > 1:
|
if len(self._pages) > 1:
|
||||||
# Paged layout
|
# Multi paged layout
|
||||||
await self.toggle_selector_refresh()
|
self._layout = (
|
||||||
self._layout = [
|
action_row,
|
||||||
(self.new_pressed, self.edit_pressed, self.del_pressed),
|
main_row,
|
||||||
(self.toggle_selector,),
|
sub_row,
|
||||||
(self.prev_page_button, self.close_pressed, self.next_page_button)
|
(self.prev_page_button, self.save_button,
|
||||||
]
|
self.refresh_button, self.quit_button, self.next_page_button)
|
||||||
if self._deleting:
|
|
||||||
await self.delete_selector_refresh()
|
)
|
||||||
self._layout.append((self.delete_selector,))
|
|
||||||
self._layout[0] = (*self._layout[0], self.clear_pressed)
|
|
||||||
elif len(self.tasklist.tasklist) > 0:
|
elif len(self.tasklist.tasklist) > 0:
|
||||||
# Single page, with tasks
|
# Single page, but still at least one task
|
||||||
await self.toggle_selector_refresh()
|
self._layout = (
|
||||||
self._layout = [
|
action_row,
|
||||||
(self.new_pressed, self.edit_pressed, self.del_pressed, self.close_pressed),
|
main_row,
|
||||||
(self.toggle_selector,),
|
sub_row,
|
||||||
]
|
(self.save_button, self.refresh_button, self.quit_pressed)
|
||||||
if self._deleting:
|
)
|
||||||
await self.delete_selector_refresh()
|
|
||||||
self._layout[0] = (*self._layout[0], self.clear_pressed)
|
|
||||||
self._layout.append((self.delete_selector,))
|
|
||||||
else:
|
else:
|
||||||
# With no tasks, nothing to select
|
# No tasks
|
||||||
self._layout = [
|
self._layout = (
|
||||||
(self.new_pressed, self.edit_pressed, self.close_pressed)
|
action_row,
|
||||||
]
|
(self.refresh_button, self.quit_pressed)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def redraw(self, interaction: Optional[discord.Interaction] = None):
|
||||||
|
self.current_page = await self.get_page(self.page_num)
|
||||||
|
await self.refresh_components()
|
||||||
|
|
||||||
# Resend
|
# Resend
|
||||||
if not self._message:
|
if interaction is not None:
|
||||||
self._message = await self.channel.send(**self.current_page.send_args, view=self)
|
if self._message:
|
||||||
else:
|
try:
|
||||||
|
await self._message.delete()
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
self._message = await interaction.followup.send(**self.current_page.send_args, view=self)
|
||||||
|
elif self._message:
|
||||||
await self._message.edit(**self.current_page.edit_args, view=self)
|
await self._message.edit(**self.current_page.edit_args, view=self)
|
||||||
|
else:
|
||||||
|
self._message = await self.channel.send(**self.current_page.send_args, view=self)
|
||||||
|
|||||||
@@ -200,7 +200,11 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]):
|
|||||||
@property
|
@property
|
||||||
def long_desc(self):
|
def long_desc(self):
|
||||||
t = ctx_translator.get().t
|
t = ctx_translator.get().t
|
||||||
return t(self._long_desc)
|
bot = ctx_bot.get()
|
||||||
|
return t(self._long_desc).format(
|
||||||
|
bot=bot,
|
||||||
|
cmds=bot.core.mention_cache
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_name(self):
|
def display_name(self):
|
||||||
@@ -210,12 +214,20 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]):
|
|||||||
@property
|
@property
|
||||||
def desc(self):
|
def desc(self):
|
||||||
t = ctx_translator.get().t
|
t = ctx_translator.get().t
|
||||||
return t(self._desc)
|
bot = ctx_bot.get()
|
||||||
|
return t(self._desc).format(
|
||||||
|
bot=bot,
|
||||||
|
cmds=bot.core.mention_cache
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def accepts(self):
|
def accepts(self):
|
||||||
t = ctx_translator.get().t
|
t = ctx_translator.get().t
|
||||||
return t(self._accepts)
|
bot = ctx_bot.get()
|
||||||
|
return t(self._accepts).format(
|
||||||
|
bot=bot,
|
||||||
|
cmds=bot.core.mention_cache
|
||||||
|
)
|
||||||
|
|
||||||
async def write(self, **kwargs) -> None:
|
async def write(self, **kwargs) -> None:
|
||||||
await super().write(**kwargs)
|
await super().write(**kwargs)
|
||||||
|
|||||||
Reference in New Issue
Block a user