mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
Compare commits
9 Commits
67de1f6c02
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
| e817e5778b | |||
| 289034d0c8 | |||
| 2a23f4d14c | |||
| b20d355527 | |||
| fa9620eac1 | |||
| a201b62c04 | |||
| 9bf164ab77 | |||
| 97c6f3490c | |||
| f5cb856d3d |
@ -4,9 +4,10 @@
|
||||
v-for="(_, i) in amount"
|
||||
:key="i"
|
||||
:class="[
|
||||
carousel.currentSlide == i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
|
||||
carousel.currentSlide === i ? 'bg-blue-600 w-6' : 'bg-zinc-700 w-3',
|
||||
'transition-all cursor-pointer h-2 rounded-full',
|
||||
]"
|
||||
@click="slideTo(i)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -18,8 +19,8 @@ const carousel = inject(injectCarousel)!;
|
||||
|
||||
const amount = carousel.maxSlide - carousel.minSlide + 1;
|
||||
|
||||
// function slideTo(index: number) {
|
||||
// const offsetIndex = index + carousel.minSlide;
|
||||
// carousel.nav.slideTo(offsetIndex);
|
||||
// }
|
||||
function slideTo(index: number) {
|
||||
const offsetIndex = index + carousel.minSlide;
|
||||
carousel.nav.slideTo(offsetIndex);
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -29,6 +29,23 @@
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 pt-8">
|
||||
<MultiItemSelector v-model="currentTags" :items="tags" />
|
||||
<div class="flex flex-col">
|
||||
<label
|
||||
for="releaseDate"
|
||||
class="text-sm/6 font-medium text-zinc-100"
|
||||
>
|
||||
{{ $t("library.admin.game.editReleaseDate") }}
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="releaseDate"
|
||||
v-model="releaseDate"
|
||||
type="date"
|
||||
name="releaseDate"
|
||||
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- image carousel pick -->
|
||||
@ -491,11 +508,38 @@ watch(
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const releaseDate = ref(
|
||||
game.value.mReleased
|
||||
? new Date(game.value.mReleased).toISOString().substring(0, 10)
|
||||
: "",
|
||||
);
|
||||
|
||||
watch(releaseDate, async (newDate) => {
|
||||
const body: PatchGameBody = {};
|
||||
|
||||
if (newDate) {
|
||||
const parsed = new Date(newDate);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
body.mReleased = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
await $dropFetch(`/api/v1/admin/game/:id`, {
|
||||
method: "PATCH",
|
||||
params: {
|
||||
id: game.value.id,
|
||||
},
|
||||
body,
|
||||
failTitle: "Failed to update release date",
|
||||
});
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// I don't know why I split these fields off.
|
||||
const coreMetadataName = ref(game.value.mName);
|
||||
const coreMetadataDescription = ref(game.value.mShortDescription);
|
||||
|
||||
const coreMetadataIconUrl = ref(useObject(game.value.mIconObjectId));
|
||||
const coreMetadataIconFileUpload = ref<FileList | undefined>();
|
||||
const coreMetadataLoading = ref(false);
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
<i18n>
|
||||
{
|
||||
"en": {
|
||||
"↓": "↓",
|
||||
"↑": "↑"
|
||||
}
|
||||
}
|
||||
</i18n>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
@ -176,9 +185,12 @@
|
||||
active ? 'bg-zinc-900 outline-hidden' : '',
|
||||
'w-full text-left block px-4 py-2 text-sm',
|
||||
]"
|
||||
@click="() => (currentSort = option.param)"
|
||||
@click.prevent="handleSortClick(option, $event)"
|
||||
>
|
||||
{{ option.name }}
|
||||
<span v-if="currentSort === option.param">
|
||||
{{ sortOrder === "asc" ? $t("↑") : $t("↓") }}
|
||||
</span>
|
||||
</button>
|
||||
</MenuItem>
|
||||
</div>
|
||||
@ -292,7 +304,7 @@
|
||||
<div
|
||||
v-if="games?.length ?? 0 > 0"
|
||||
ref="product-grid"
|
||||
class="relative lg:col-span-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-7 gap-4"
|
||||
class="relative lg:col-span-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4"
|
||||
>
|
||||
<!-- Your content -->
|
||||
<GamePanel
|
||||
@ -389,8 +401,13 @@ const sorts: Array<StoreSortOption> = [
|
||||
name: "Recently Added",
|
||||
param: "recent",
|
||||
},
|
||||
{
|
||||
name: "Name",
|
||||
param: "name",
|
||||
},
|
||||
];
|
||||
const currentSort = ref(sorts[0].param);
|
||||
const sortOrder = ref<"asc" | "desc">("desc");
|
||||
|
||||
const options: Array<StoreFilterOption> = [
|
||||
...(tags.length > 0
|
||||
@ -466,7 +483,7 @@ async function updateGames(query: string, resetGames: boolean) {
|
||||
results: Array<SerializeObject<GameModel>>;
|
||||
count: number;
|
||||
}>(
|
||||
`/api/v1/store?take=50&skip=${resetGames ? 0 : games.value?.length || 0}&sort=${currentSort.value}${query ? "&" + query : ""}`,
|
||||
`/api/v1/store?take=50&skip=${resetGames ? 0 : games.value?.length || 0}&sort=${currentSort.value}&order=${sortOrder.value}${query ? "&" + query : ""}`,
|
||||
);
|
||||
if (resetGames) {
|
||||
games.value = newValues.results;
|
||||
@ -483,6 +500,19 @@ watch(filterQuery, (newUrl) => {
|
||||
watch(currentSort, (_) => {
|
||||
updateGames(filterQuery.value, true);
|
||||
});
|
||||
watch(sortOrder, (_) => {
|
||||
updateGames(filterQuery.value, true);
|
||||
});
|
||||
|
||||
await updateGames(filterQuery.value, true);
|
||||
|
||||
function handleSortClick(option: StoreSortOption, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
if (currentSort.value === option.param) {
|
||||
sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
currentSort.value = option.param;
|
||||
sortOrder.value = option.param === "name" ? "asc" : "desc";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -292,6 +292,7 @@
|
||||
"deleteImage": "Delete image",
|
||||
"editGameDescription": "Game Description",
|
||||
"editGameName": "Game Name",
|
||||
"editReleaseDate": "Release Date",
|
||||
"imageCarousel": "Image Carousel",
|
||||
"imageCarouselDescription": "Customise what images and what order are shown on the store page.",
|
||||
"imageCarouselEmpty": "No images added to the carousel yet.",
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "^16.0.1",
|
||||
"@drop-oss/droplet": "3.0.1",
|
||||
"@drop-oss/droplet": "3.2.0",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@lobomfz/prismark": "0.0.3",
|
||||
@ -33,7 +33,7 @@
|
||||
"@vueuse/nuxt": "13.6.0",
|
||||
"argon2": "^0.43.0",
|
||||
"arktype": "^2.1.10",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "^1.12.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"cookie-es": "^2.0.0",
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
{{ $t("store.images") }}
|
||||
</h2>
|
||||
<div class="relative">
|
||||
<VueCarousel :items-to-show="1">
|
||||
<VueCarousel :items-to-show="1" :wrap-around="true">
|
||||
<VueSlide
|
||||
v-for="image in game.mImageCarouselObjectIds"
|
||||
:key="image"
|
||||
|
||||
@ -183,7 +183,7 @@
|
||||
{{ game.mShortDescription }}
|
||||
</p>
|
||||
<div class="mt-6 py-4 rounded">
|
||||
<VueCarousel :items-to-show="1">
|
||||
<VueCarousel :items-to-show="1" :wrap-around="true">
|
||||
<VueSlide
|
||||
v-for="image in game.mImageCarouselObjectIds"
|
||||
:key="image"
|
||||
|
||||
459
pnpm-lock.yaml
generated
459
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1 +1,4 @@
|
||||
overrides:
|
||||
droplet: link:../../.local/share/pnpm/global/5/node_modules/@drop-oss/droplet
|
||||
|
||||
shamefullyHoist: true
|
||||
|
||||
@ -18,7 +18,8 @@ const StoreRead = type({
|
||||
company: "string?",
|
||||
companyActions: "string = 'published,developed'",
|
||||
|
||||
sort: "'default' | 'newest' | 'recent' = 'default'",
|
||||
sort: "'default' | 'newest' | 'recent' | 'name' = 'default'",
|
||||
order: "'asc' | 'desc' = 'desc'",
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
@ -101,10 +102,13 @@ export default defineEventHandler(async (h3) => {
|
||||
switch (options.sort) {
|
||||
case "default":
|
||||
case "newest":
|
||||
sort.mReleased = "desc";
|
||||
sort.mReleased = options.order;
|
||||
break;
|
||||
case "recent":
|
||||
sort.created = "desc";
|
||||
sort.created = options.order;
|
||||
break;
|
||||
case "name":
|
||||
sort.mName = options.order;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@ -191,7 +191,7 @@ export class GiantBombProvider implements MetadataProvider {
|
||||
|
||||
const res = await publisher(pub.name);
|
||||
if (res === undefined) {
|
||||
context?.logger.warn(`Failed to import publisher "${pub}"`);
|
||||
context?.logger.warn(`Failed to import publisher "${pub.name}"`);
|
||||
continue;
|
||||
}
|
||||
context?.logger.info(`Imported publisher "${pub.name}"`);
|
||||
@ -208,10 +208,10 @@ export class GiantBombProvider implements MetadataProvider {
|
||||
|
||||
const res = await developer(dev.name);
|
||||
if (res === undefined) {
|
||||
context?.logger.warn(`Failed to import developer "${dev}"`);
|
||||
context?.logger.warn(`Failed to import developer "${dev.name}"`);
|
||||
continue;
|
||||
}
|
||||
context?.logger.info(`Imported developer "${dev}"`);
|
||||
context?.logger.info(`Imported developer "${dev.name}"`);
|
||||
developers.push(res);
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ interface IGDBCompanyWebsite extends IGDBItem {
|
||||
}
|
||||
|
||||
interface IGDBCover extends IGDBItem {
|
||||
url: string;
|
||||
image_id: string;
|
||||
}
|
||||
|
||||
interface IGDBSearchStub extends IGDBItem {
|
||||
@ -179,7 +179,7 @@ export class IGDBProvider implements MetadataProvider {
|
||||
|
||||
if (response.status !== 200)
|
||||
throw new Error(
|
||||
`Error in IDGB \nStatus Code: ${response.status}\n${response.data}`,
|
||||
`Error in IGDB \nStatus Code: ${response.status}\n${response.data}`,
|
||||
);
|
||||
|
||||
this.accessToken = response.data.access_token;
|
||||
@ -187,7 +187,7 @@ export class IGDBProvider implements MetadataProvider {
|
||||
seconds: response.data.expires_in,
|
||||
});
|
||||
|
||||
logger.info("IDGB done authorizing with twitch");
|
||||
logger.info("IGDB done authorizing with twitch");
|
||||
}
|
||||
|
||||
private async refreshCredentials() {
|
||||
@ -246,39 +246,47 @@ export class IGDBProvider implements MetadataProvider {
|
||||
return <T[]>response.data;
|
||||
}
|
||||
|
||||
private async _getMediaInternal(mediaID: IGDBID, type: string) {
|
||||
private async _getMediaInternal(
|
||||
mediaID: IGDBID,
|
||||
type: string,
|
||||
size: string = "t_thumb",
|
||||
) {
|
||||
if (mediaID === undefined)
|
||||
throw new Error(
|
||||
`IGDB mediaID when getting item of type ${type} was undefined`,
|
||||
);
|
||||
|
||||
const body = `where id = ${mediaID}; fields url;`;
|
||||
const body = `where id = ${mediaID}; fields image_id;`;
|
||||
const response = await this.request<IGDBCover>(type, body);
|
||||
|
||||
let result = "";
|
||||
if (!response.length || !response[0].image_id) {
|
||||
throw new Error(`No image_id found for ${type} with id ${mediaID}`);
|
||||
}
|
||||
|
||||
response.forEach((cover) => {
|
||||
if (cover.url.startsWith("https:")) {
|
||||
result = cover.url;
|
||||
} else {
|
||||
// twitch *sometimes* provides it in the format "//images.igdb.com"
|
||||
result = `https:${cover.url}`;
|
||||
}
|
||||
});
|
||||
const imageId = response[0].image_id;
|
||||
const result = `https://images.igdb.com/igdb/image/upload/${size}/${imageId}.jpg`;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getCoverURL(id: IGDBID) {
|
||||
return await this._getMediaInternal(id, "covers");
|
||||
return await this._getMediaInternal(id, "covers", "t_cover_big");
|
||||
}
|
||||
|
||||
private async getArtworkURL(id: IGDBID) {
|
||||
return await this._getMediaInternal(id, "artworks");
|
||||
return await this._getMediaInternal(id, "artworks", "t_1080p");
|
||||
}
|
||||
|
||||
private async getScreenshotURL(id: IGDBID) {
|
||||
return await this._getMediaInternal(id, "screenshots", "t_1080p");
|
||||
}
|
||||
|
||||
private async getIconURL(id: IGDBID) {
|
||||
return await this._getMediaInternal(id, "covers", "t_thumb");
|
||||
}
|
||||
|
||||
private async getCompanyLogoURl(id: IGDBID) {
|
||||
return await this._getMediaInternal(id, "company_logos");
|
||||
return await this._getMediaInternal(id, "company_logos", "t_original");
|
||||
}
|
||||
|
||||
private trimMessage(msg: string, len: number) {
|
||||
@ -327,7 +335,7 @@ export class IGDBProvider implements MetadataProvider {
|
||||
let icon = "";
|
||||
const cover = response[i].cover;
|
||||
if (cover !== undefined) {
|
||||
icon = await this.getCoverURL(cover);
|
||||
icon = await this.getIconURL(cover);
|
||||
} else {
|
||||
icon = "";
|
||||
}
|
||||
@ -355,23 +363,26 @@ export class IGDBProvider implements MetadataProvider {
|
||||
const currentGame = (await this.request<IGDBGameFull>("games", body)).at(0);
|
||||
if (!currentGame) throw new Error("No game found on IGDB with that id");
|
||||
|
||||
context?.logger.info("Using IDGB provider.");
|
||||
context?.logger.info("Using IGDB provider.");
|
||||
|
||||
let iconRaw;
|
||||
let iconRaw, coverRaw;
|
||||
const cover = currentGame.cover;
|
||||
|
||||
if (cover !== undefined) {
|
||||
context?.logger.info("Found cover URL, using...");
|
||||
iconRaw = await this.getCoverURL(cover);
|
||||
iconRaw = await this.getIconURL(cover);
|
||||
coverRaw = await this.getCoverURL(cover);
|
||||
} else {
|
||||
context?.logger.info("Missing cover URL, using fallback...");
|
||||
iconRaw = jdenticon.toPng(id, 512);
|
||||
coverRaw = iconRaw;
|
||||
}
|
||||
|
||||
const icon = createObject(iconRaw);
|
||||
const coverID = createObject(coverRaw);
|
||||
let banner;
|
||||
|
||||
const images = [icon];
|
||||
const images = [coverID];
|
||||
for (const art of currentGame.artworks ?? []) {
|
||||
const objectId = createObject(await this.getArtworkURL(art));
|
||||
if (!banner) {
|
||||
@ -384,6 +395,11 @@ export class IGDBProvider implements MetadataProvider {
|
||||
banner = createObject(jdenticon.toPng(id, 512));
|
||||
}
|
||||
|
||||
for (const screenshot of currentGame.screenshots ?? []) {
|
||||
const objectId = createObject(await this.getScreenshotURL(screenshot));
|
||||
images.push(objectId);
|
||||
}
|
||||
|
||||
context?.progress(20);
|
||||
|
||||
const publishers: CompanyModel[] = [];
|
||||
@ -452,13 +468,25 @@ export class IGDBProvider implements MetadataProvider {
|
||||
|
||||
const genres = await this.getGenres(currentGame.genres);
|
||||
|
||||
const deck = this.trimMessage(currentGame.summary, 280);
|
||||
let description = "";
|
||||
let shortDescription = "";
|
||||
|
||||
if (currentGame.summary.length > (currentGame.storyline?.length ?? 0)) {
|
||||
description = currentGame.summary;
|
||||
shortDescription = this.trimMessage(
|
||||
currentGame.storyline ?? currentGame.summary,
|
||||
280,
|
||||
);
|
||||
} else {
|
||||
description = currentGame.storyline ?? currentGame.summary;
|
||||
shortDescription = this.trimMessage(currentGame.summary, 280);
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
id: currentGame.id.toString(),
|
||||
name: currentGame.name,
|
||||
shortDescription: deck,
|
||||
description: currentGame.summary,
|
||||
shortDescription,
|
||||
description,
|
||||
released,
|
||||
|
||||
genres,
|
||||
@ -471,7 +499,7 @@ export class IGDBProvider implements MetadataProvider {
|
||||
|
||||
icon,
|
||||
bannerId: banner,
|
||||
coverId: icon,
|
||||
coverId: coverID,
|
||||
images,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user