Repository navigation
PageViewer
What this page covers: the chat renderer of a Ranking list - the paginated page every
/list, /baltop and /top style command sends: how it is written for each reader, its theme, and
what clicking a page button actually runs.
A page separates the expensive half from the cheap half. Reading the source, ordering it, cutting it
and resolving the values every reader shares belongs to the RankedList and is cached there as a
RankedSnapshot. The text is cheap and differs per reader - language, their own position on the list
- so the
PageViewerbuilds it at send time and never caches it.
import br.com.finalcraft.evernifecore.pageviewer.PageViewer;
import br.com.finalcraft.evernifecore.pageviewer.theme.PageTheme;
import br.com.finalcraft.evernifecore.ranking.RankedList;
PageViewer<FPlayer> online = PageViewer.of(
RankedList.of(plugin, "online", FPlayer.class) // reachable as /ecpage myplugin:online 3
.source(() -> EverNifeCore.getPlatform().getOnlinePlayers())
.unlimitedEntries() // required step - see Ranking
.orderBy(FPlayer::getName).ascending()
.build())
.setFormatLine("&7#${number}: &a${player}")
.theme(PageTheme.classic().withTotalCount())
.build();
online.send(page, sender);Build the page once and keep it; send is what you call per invocation. Source, ceiling, order,
cache and the shared values (addValue) are the list's - see Ranking. The page adds the format,
the theme, the page size (setPageSize, default 10), the navigation and the per-reader placeholders.
.setFormatLine("&7#${number}: &a${value}") // same text for everybody
.setFormatLine(FancyText.of("...").setHover("...")) // same, with hover/click
.setFormatLine(LINE_MESSAGE) // a LocaleMessage: each reader's language
.setFormatLine(entry -> new FancySegment(...)) // a fresh FancyText per entryA String or a FancyText does not know about languages, so it answers the same thing to everybody
- in practice, the default language. A Localization
LocaleMessageanswersgetFancyText(reader). The same overloads exist forsetFormatHeader(...)andsetFormatFooter(...), which take varargs (one line each).
Anything sendable is also a line, which is how a decorated message becomes one - custom() hands
back a per-send decoration, and appending joins two locale messages into a single line:
.setFormatLine(TOP_LINE.custom()
.setHover("&7Click for details of ${player}")
.append(TOP_LINE_SUFFIX))| Key | Value |
|---|---|
${number} |
The entry's 1-based position in the whole list, not in the current page. |
${value} |
What orderBy extracted, or the entry itself on a list with no order. |
${player} |
The entry's name, on a list whose target type is an FPlayer or an IPlayerData. Answers null for a row read straight from storage - see Ranking. |
Each of them is a default: declare the same key with addValue on the list and yours wins.
RankedList.of(plugin, "balance", FEPlayerData.class)
...
.addValue("balance", FEPlayerData::getMoneyFormatted) // on the list
.build();
PageViewer.of(list)
.addViewerPlaceholder("highlight", (row, reader) -> // on the page
row.getUniqueId().equals(reader.getUniqueId()) ? "&6&l" : "&e")| Level | Resolved | Cost |
|---|---|---|
RankedList addValue(key, Function<T, Object>)
|
once per entry per snapshot, shared by every reader, page and placeholder | 1 call per entry |
PageViewer addViewerPlaceholder(key, BiFunction<T, FCommandSender, Object>)
|
in each recipient's own render | 1 call per line per reader |
A viewer placeholder is not computed where the line does not cite the key, and a key cited twice on
the same line (text and hover) is resolved once. addValue is the one to reach for by default.
Keys are declared bare - "balance", never "${balance}". See Placeholders.
.theme(PageTheme.classic()) // a rule above the entries
.theme(PageTheme.classic().withDate().withTotalCount())
.theme(PageTheme.autoFit()) // the rule measured against the chat width
.theme(PageTheme.none()) // entries onlyEvery line of a theme is a locale message, so restyling or translating a page is editing
lang_XX.yml - the same road as any other message of the core.
classic() writes the rule out as 53 dashes, the number that fills a 320px chat line. autoFit()
measures it instead, which depends on the client: chat width, GUI scale, forced unicode font and
resource packs are all its options, and the server cannot see any of them. On a platform that cannot
measure text at all, autoFit() hands its input straight back rather than guessing.
A page with no entries prints the theme's empty state instead of buttons pointing at page 1 of 0.
What a page button runs. A page over a named list (RankedList.of) navigates by the list's id; one over
an anonymous list (RankedList.anonymous) opens a session for its reader. Either is overridden by
navigation(...).
| Strategy | Memory | The link | Re-runs the command |
|---|---|---|---|
PageNavigation.command("/finaljobs top %page%") |
none | /finaljobs top 3 |
yes |
PageNavigation.registered("finaljobs:top") |
none | /ecpage finaljobs:top 3 |
no |
PageNavigation.session() |
one entry per reader, 10 min | /ecpage 8f3a1c...-b1 3 |
no |
PageNavigation.none() |
none | no bar at all | - |
command(...) runs the caller's own command line again, side effects and all. Only for a command
that is idempotent and cheap; one that charges the player, consumes a cooldown or writes an audit
entry must not use it.
registered(...) needs a list whose content does not depend on arguments, because the id has to
mean the same thing tomorrow. Building a RankedList.of(owner, name, ...) is the whole registration;
/ecpage <id> <page> sends the renderer the last PageViewer.of(list).build() attached, which
PageViewer.attachedTo(list) answers (null for an anonymous list or one never paged). The list itself
knows nothing of its chat renderer: keep the PageViewer you built in a field and send through it.
session() is the fallback for a page that depends on an argument and therefore cannot be named.
The handle belongs to one reader - somebody else holding it is answered exactly like a handle that
expired - and it does not survive a relog. When it is gone, the page says so and names the way out
("run the command again").
Both paths depend on the link being stable, re-executable text:
-
The client's command history. Clicking a button runs a command, and that command enters the
history. Up-arrow after a relog brings
/ecpage finaljobs:top 3back, on a vanilla client. - Mods that keep the chat across relogs. The clickable button comes back whole.
Which is why a named page is worth naming: /ecpage finaljobs:top 3 still means the same page
tomorrow, and a session handle does not.
viewer.send(sender); // page 1
viewer.send(Integer.valueOf(3), sender);
viewer.send(pageVisualization, sender); // the [page] argument - see Argument Parsing
viewer.send(sender, otherSender); // one page, several recipients, one source readA list with a plain source is delivered inline, on the sender's thread. A list with an async source is
delivered when its read lands, on the main thread (ECScheduler.onMainThread); the command scope
(${label}) is captured at the send. A read that fails with no previous snapshot re-throws its cause
to the sender's command, as a plain source that throws always did.
PageVisualization is what the [page] argument parses into: a page, a range (2-4) or all, the
last two behind their own permission nodes. all shows everything the page holds - which is
everything the ceiling let in, with the rest still counted in the total.
A command executor is instantiated before its @FCLocale fields are filled, so a page built in a
field initialiser would capture a message that is still null. Build it lazily:
public class CMDList {
@FCLocale(lang = LocaleType.EN_US, text = "&7# ${number}: &a${player}")
private static LocaleMessage LINE;
// Initialised on first use, which is after the locale scan has run.
private static final class Page {
static final PageViewer<FPlayer> ONLINE = PageViewer.of(
RankedList.of(EverNifeCore.getEcPluginData(), "list", FPlayer.class)
.source(() -> EverNifeCore.getPlatform().getOnlinePlayers())
.unlimitedEntries()
.orderBy(FPlayer::getName).ascending()
.build())
.setFormatLine(LINE)
.build();
}
@FinalCMD(aliases = {"playerlist"})
public void onCommand(FCommandSender sender, @Arg("[page]") PageVisualization page) {
Page.ONLINE.send(page, sender);
}
}The core's real list is /playerlist, and it also takes /list while Settings.Commands.LIST_ALIAS in
EverNifeCore's config.yml is true (the default). Set it to false to keep the server's own /list.
- Ranking - the list a page renders: source, ceiling, order, cache, values
- FancyText - the rich-text model a line is written in
-
Placeholders - the
${key}engine and its rules -
Localization -
LocaleMessageand per-player language -
PlayerData-and-PDSections - sections, their cache policy and the indexed query API (
querySections) -
Argument Parsing - the
[page]argument
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Block Data (SVWorldDataManager)
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Logging & Debug Modules
- Events
- Regions
- Groups
- Notifications
- NPCs
- Logic
- Quests
- Provenance
- Scheduler & Threading
- Items & NBT
- Screens
- Dialogues
- Menus
- Integrations
- Economy
- Boosters
- Deliveries
- Audit Log
- Dynamic Values
- Player Ignore
- Version Compatibility
Architecture & Reference