Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
498 changes: 498 additions & 0 deletions components/servers/ServerMapRotation.vue

Large diffs are not rendered by default.

250 changes: 250 additions & 0 deletions components/servers/ServerPlugins.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useQuery } from "@vue/apollo-composable";
import gql from "graphql-tag";
import { Button } from "~/components/ui/button";
import AnimatedFilters from "~/components/common/AnimatedFilters.vue";
import { toast } from "@/components/ui/toast";

type Choice = "default" | "on" | "off";

type Install = {
plugin_slug: string;
load_custom: boolean;
load_tournaments: boolean;
plugin: { name: string; map_rotation: unknown | null } | null;
};

const props = defineProps<{ serverId: string }>();

const { t } = useI18n();
const nuxtApp = useNuxtApp();

// Raw documents: server_plugins and setServerPlugins are newer than
// ~/generated/zeus until codegen runs against a migrated database.
const QUERY = gql`
query ServerPlugins($serverId: uuid!) {
servers_by_pk(id: $serverId) {
id
game_mode {
id
name
plugins {
plugin_slug
}
}
plugin_overrides {
plugin_slug
enabled
}
}
game_plugin_installs(
where: { enabled: { _eq: true } }
order_by: { plugin_slug: asc }
) {
plugin_slug
load_custom
load_tournaments
plugin {
name
map_rotation
}
}
}
`;

const SAVE = gql`
mutation SetServerPlugins(
$serverId: uuid!
$plugins: [ServerPluginInput!]!
) {
setServerPlugins(server_id: $serverId, plugins: $plugins) {
success
}
}
`;

const { result, refetch } = useQuery(
QUERY,
() => ({ serverId: props.serverId }),
{ fetchPolicy: "cache-and-network" },
);

const choices = ref<Record<string, Choice>>({});
const dirty = ref(false);

function reset() {
const overrides = (result.value as any)?.servers_by_pk?.plugin_overrides;

choices.value = Object.fromEntries(
(overrides ?? []).map(
(override: { plugin_slug: string; enabled: boolean }) => [
override.plugin_slug,
override.enabled ? "on" : "off",
],
),
);
dirty.value = false;
}

watch(
result,
() => {
if (!dirty.value) {
reset();
}
},
{ immediate: true },
);

const installs = computed<Array<Install>>(
() => (result.value as any)?.game_plugin_installs ?? [],
);

const gameMode = computed(
() => (result.value as any)?.servers_by_pk?.game_mode,
);

// A mode's plugins load whenever the mode does; the api does not let a server
// switch them off, so they are shown rather than offered.
const modePlugins = computed(
() =>
new Set<string>(
(gameMode.value?.plugins ?? []).map(
(plugin: { plugin_slug: string }) => plugin.plugin_slug,
),
),
);

const options = computed(() => [
{
key: "default",
label: t("pages.dedicated_servers.detail.plugins.default"),
},
{ key: "on", label: t("pages.dedicated_servers.detail.plugins.on") },
{ key: "off", label: t("pages.dedicated_servers.detail.plugins.off") },
]);

function choiceFor(slug: string): Choice {
return choices.value[slug] ?? "default";
}

function setChoice(slug: string, choice: Choice) {
choices.value = { ...choices.value, [slug]: choice };
dirty.value = true;
}

// A community server has no match to scope by, so it takes every plugin
// flagged for tournaments or custom matches.
function defaultHint(install: Install) {
return install.load_custom || install.load_tournaments
? t("pages.dedicated_servers.detail.plugins.loads_by_default")
: t("pages.dedicated_servers.detail.plugins.off_by_default");
}

async function save() {
const plugins = Object.entries(choices.value)
.filter(
([slug, choice]) => choice !== "default" && !modePlugins.value.has(slug),
)
.map(([slug, choice]) => ({ slug, enabled: choice === "on" }));

await nuxtApp.$apollo.defaultClient.mutate({
mutation: SAVE,
variables: { serverId: props.serverId, plugins },
});

dirty.value = false;

toast({ title: t("pages.dedicated_servers.detail.plugins.saved") });

await refetch();
}
</script>

<template>
<div>
<div
class="mb-3 inline-flex items-center gap-2 font-mono text-[0.7rem] uppercase tracking-[0.22em] text-muted-foreground"
>
<span class="h-[2px] w-[10px] bg-[hsl(var(--tac-amber))]"></span>
{{ $t("pages.dedicated_servers.detail.plugins.title") }}
</div>

<div class="space-y-4 rounded-lg border border-border bg-muted/30 p-4">
<p class="text-sm text-muted-foreground">
{{ $t("pages.dedicated_servers.detail.plugins.description") }}
</p>

<ul v-if="installs.length" class="divide-y divide-border/60">
<li
v-for="install in installs"
:key="install.plugin_slug"
class="flex items-center justify-between gap-4 py-2.5 max-sm:flex-col max-sm:items-start"
>
<div class="min-w-0 space-y-0.5">
<NuxtLink
:to="`/plugins/${install.plugin_slug}`"
class="text-sm font-medium hover:underline"
>
{{ install.plugin?.name ?? install.plugin_slug }}
</NuxtLink>
<p class="text-xs text-muted-foreground">
{{ defaultHint(install) }}
<template v-if="install.plugin?.map_rotation">
·
{{ $t("pages.dedicated_servers.detail.plugins.map_rotation") }}
</template>
</p>
</div>

<span
v-if="modePlugins.has(install.plugin_slug)"
class="inline-flex shrink-0 items-center rounded border border-border/70 bg-muted/35 px-2.5 py-1 font-mono text-[0.62rem] font-bold uppercase tracking-[0.14em] text-muted-foreground"
>
{{
$t("pages.dedicated_servers.detail.plugins.from_mode", {
mode: gameMode?.name,
})
}}
</span>
<AnimatedFilters
v-else
square
:options="options"
:model-value="choiceFor(install.plugin_slug)"
@update:model-value="
(choice) => setChoice(install.plugin_slug, choice as Choice)
"
/>
</li>
</ul>
<div
v-else
class="flex items-center justify-between gap-3 text-sm text-muted-foreground max-sm:flex-col max-sm:items-start"
>
<span>{{ $t("pages.dedicated_servers.detail.plugins.empty") }}</span>
<Button as-child variant="outline" size="sm">
<NuxtLink to="/plugins">
{{ $t("pages.dedicated_servers.detail.plugins.browse") }}
</NuxtLink>
</Button>
</div>

<div
class="flex items-center justify-end gap-3 border-t border-border/60 pt-4 max-sm:flex-col max-sm:items-stretch"
>
<p class="mr-auto text-xs text-muted-foreground">
{{ $t("pages.dedicated_servers.detail.restart_hint") }}
</p>
<Button v-if="dirty" variant="ghost" @click="reset">
{{ $t("pages.dedicated_servers.detail.discard") }}
</Button>
<Button :disabled="!dirty" @click="save">
{{ $t("pages.dedicated_servers.detail.save_restart") }}
</Button>
</div>
</div>
</div>
</template>
40 changes: 40 additions & 0 deletions i18n/locales/ar_SA.json
Original file line number Diff line number Diff line change
Expand Up @@ -3687,6 +3687,46 @@
"title": "هل أنت متأكد تمامًا؟",
"description": "لا يمكن التراجع عن هذا الإجراء. سيؤدي ذلك إلى حذف سيرفرك نهائيًا وإزالة البيانات المرتبطة به من سيرفراتنا.",
"continue": "متابعة"
},
"save_restart": "حفظ وإعادة التشغيل",
"discard": "تجاهل",
"restart_hint": "يؤدي الحفظ إلى إعادة تشغيل الخادم وفصل كل من يلعب عليه.",
"map_rotation": {
"title": "تدوير الخرائط",
"description": "الخرائط التي يتنقل بينها هذا الخادم. يبدأ على الخريطة الأولى، أو على خريطة عشوائية عند تفعيل الخلط، ثم تتولى إضافة تدوير الخرائط تصويتات نهاية الخريطة وrock the vote والتدوير التلقائي.",
"shuffle": "خلط",
"shuffle_hint": "اختيار الخريطة التالية عشوائيًا",
"played_by": "تديره {plugin}",
"no_plugin": "لا توجد إضافة مثبتة لتدوير الخرائط، لذا سيبقى الخادم على خريطته الأولى.",
"install_plugin": "تثبيت MapChooser",
"workshop_order_hint": "تفقد MapChooser موضعها في القائمة مع خرائط الورشة وتعود دائمًا إلى الخريطة الثانية. أبقِ الخلط مفعّلًا لخرائط الورشة.",
"empty": "لا توجد خرائط بعد. أضف خرائط من الكتالوج أو استورد مجموعة من ورشة Steam.",
"official": "رسمية",
"workshop": "الورشة",
"add_map": "إضافة خريطة",
"add": "إضافة",
"import_placeholder": "رابط أو معرّف مجموعة الورشة",
"import": "استيراد",
"imported": "خرائط الورشة المضافة: {count}",
"imported_skipped": "تم تخطيها لأنها ليست خرائط CS2: {count}",
"move_up": "نقل لأعلى",
"move_down": "نقل لأسفل",
"remove": "إزالة",
"saved": "تم حفظ تدوير الخرائط. تتم إعادة تشغيل الخادم."
},
"plugins": {
"title": "الإضافات",
"description": "فعّل إضافة لعبة أو عطّلها لهذا الخادم فقط. الافتراضي يتبع إعدادات التحميل الخاصة بالإضافة.",
"default": "افتراضي",
"on": "تشغيل",
"off": "إيقاف",
"loads_by_default": "تُحمَّل هنا افتراضيًا",
"off_by_default": "متوقفة هنا افتراضيًا",
"map_rotation": "تدير تدوير الخرائط",
"from_mode": "من {mode}",
"empty": "لا توجد إضافات لعبة مثبتة بعد.",
"browse": "دليل الإضافات",
"saved": "تم حفظ الإضافات. تتم إعادة تشغيل الخادم."
}
}
},
Expand Down
40 changes: 40 additions & 0 deletions i18n/locales/da_DK.json
Original file line number Diff line number Diff line change
Expand Up @@ -3687,6 +3687,46 @@
"title": "Slet serveren?",
"description": "Handlingen kan ikke fortrydes. Serveren slettes permanent, og tilhørende data fjernes fra vores servere.",
"continue": "Fortsæt"
},
"save_restart": "Gem og genstart",
"discard": "Kassér",
"restart_hint": "Når du gemmer, genstartes serveren, og alle der spiller bliver afbrudt.",
"map_rotation": {
"title": "Mapsrotation",
"description": "De maps denne server skifter mellem. Den starter på den første, eller en tilfældig når der blandes, og derefter overtager rotationspluginnet med afstemninger ved mapslut, rock the vote og det automatiske skift.",
"shuffle": "Bland",
"shuffle_hint": "Vælg næste map tilfældigt",
"played_by": "Styres af {plugin}",
"no_plugin": "Der er ikke installeret et plugin til mapsrotation, så serveren bliver på sit første map.",
"install_plugin": "Installer MapChooser",
"workshop_order_hint": "MapChooser mister sin plads i listen på workshop-maps og vender altid tilbage til det andet. Hold Bland slået til for workshop-maps.",
"empty": "Ingen maps endnu. Tilføj maps fra kataloget, eller importer en Steam Workshop-samling.",
"official": "Officielle",
"workshop": "Workshop",
"add_map": "Tilføj et map",
"add": "Tilføj",
"import_placeholder": "Link eller ID til workshop-samling",
"import": "Importer",
"imported": "Workshop-maps tilføjet: {count}",
"imported_skipped": "Sprunget over, ikke CS2-maps: {count}",
"move_up": "Flyt op",
"move_down": "Flyt ned",
"remove": "Fjern",
"saved": "Mapsrotation gemt. Serveren genstarter."
},
"plugins": {
"title": "Plugins",
"description": "Slå et spilplugin til eller fra kun for denne server. Standard følger pluginnets egne indlæsningsindstillinger.",
"default": "Standard",
"on": "Til",
"off": "Fra",
"loads_by_default": "Indlæses her som standard",
"off_by_default": "Fra her som standard",
"map_rotation": "Styrer mapsrotationen",
"from_mode": "Fra {mode}",
"empty": "Der er endnu ikke installeret nogen spilplugins.",
"browse": "Plugin-oversigt",
"saved": "Plugins gemt. Serveren genstarter."
}
}
},
Expand Down
40 changes: 40 additions & 0 deletions i18n/locales/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -3687,6 +3687,46 @@
"title": "Bist du dir ganz sicher?",
"description": "Das lässt sich nicht rückgängig machen. Dein Server wird dauerhaft gelöscht und alle zugehörigen Daten werden von unseren Servern entfernt.",
"continue": "Weiter"
},
"save_restart": "Speichern & Neu starten",
"discard": "Verwerfen",
"restart_hint": "Beim Speichern wird der Server neu gestartet, alle Spieler darauf werden getrennt.",
"map_rotation": {
"title": "Map-Rotation",
"description": "Die Maps, die dieser Server durchläuft. Er startet auf der ersten, oder bei Zufallsreihenfolge auf einer zufälligen, danach übernimmt das Map-Rotations-Plugin mit Abstimmungen am Map-Ende, Rock the Vote und dem automatischen Wechsel.",
"shuffle": "Zufällig",
"shuffle_hint": "Nächste Map zufällig wählen",
"played_by": "Gespielt von {plugin}",
"no_plugin": "Kein Map-Rotations-Plugin installiert, der Server bleibt auf seiner ersten Map.",
"install_plugin": "MapChooser installieren",
"workshop_order_hint": "MapChooser verliert bei Workshop-Maps seine Position in der Liste und kehrt immer zur zweiten zurück. Lass „Zufällig“ bei Workshop-Maps an.",
"empty": "Noch keine Maps. Füge Maps aus dem Katalog hinzu oder importiere eine Steam-Workshop-Kollektion.",
"official": "Offiziell",
"workshop": "Workshop",
"add_map": "Map hinzufügen",
"add": "Hinzufügen",
"import_placeholder": "Link oder ID der Workshop-Kollektion",
"import": "Importieren",
"imported": "Workshop-Maps hinzugefügt: {count}",
"imported_skipped": "Übersprungen, keine CS2-Maps: {count}",
"move_up": "Nach oben",
"move_down": "Nach unten",
"remove": "Entfernen",
"saved": "Map-Rotation gespeichert. Der Server startet neu."
},
"plugins": {
"title": "Plugins",
"description": "Schalte ein Spiel-Plugin nur für diesen Server ein oder aus. Standard folgt den Ladeeinstellungen des Plugins.",
"default": "Standard",
"on": "An",
"off": "Aus",
"loads_by_default": "Lädt hier standardmäßig",
"off_by_default": "Hier standardmäßig aus",
"map_rotation": "Steuert die Map-Rotation",
"from_mode": "Aus {mode}",
"empty": "Noch keine Spiel-Plugins installiert.",
"browse": "Plugin-Verzeichnis",
"saved": "Plugins gespeichert. Der Server startet neu."
}
}
},
Expand Down
Loading
Loading