Merge branch 'master' into openid-connect

This commit is contained in:
Georges-Antoine Assi
2024-12-09 19:25:44 -05:00
109 changed files with 2137 additions and 1932 deletions

View File

@@ -18,25 +18,25 @@ runtimes:
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint:
enabled:
- markdownlint@0.42.0
- eslint@9.14.0
- markdownlint@0.43.0
- eslint@9.16.0
- actionlint@1.7.4
- bandit@1.7.10
- bandit@1.8.0
- black@24.10.0
- checkov@3.2.296
- checkov@3.2.332
- git-diff-check
- isort@5.13.2
- mypy@1.13.0
- osv-scanner@1.9.1
- oxipng@9.1.2
- prettier@3.3.3
- ruff@0.7.3
- oxipng@9.1.3
- prettier@3.4.2
- ruff@0.8.2
- shellcheck@0.10.0
- shfmt@3.6.0
- svgo@3.3.2
- taplo@0.9.3
- trivy@0.56.2
- trufflehog@3.83.6
- trufflehog@3.85.0
- yamllint@1.35.1
ignore:
- linters: [ALL]

View File

@@ -1,16 +0,0 @@
{
"languages": {
"Python": {
"tab_size": 4
},
"Vue.js": {
"tab_size": 2,
"formatter": {
"external": {
"command": "prettier",
"arguments": ["--stdin-filepath", "{buffer_path}"]
}
}
}
}
}

View File

@@ -117,6 +117,8 @@ UPLOAD_TIMEOUT = int(os.environ.get("UPLOAD_TIMEOUT", 600))
# LOGGING
LOGLEVEL: Final = os.environ.get("LOGLEVEL", "INFO")
FORCE_COLOR: Final = str_to_bool(os.environ.get("FORCE_COLOR", "false"))
NO_COLOR: Final = str_to_bool(os.environ.get("NO_COLOR", "false"))
# TESTING
IS_PYTEST_RUN: Final = bool(os.environ.get("PYTEST_VERSION", False))

View File

@@ -245,6 +245,7 @@ async def get_rom_content(
ZipResponse: Returns a response for nginx to serve a Zip file for multi-part roms
"""
current_username = request.user.username if request.user else "unknown"
rom = db_rom_handler.get_rom(id)
if not rom:
@@ -253,6 +254,8 @@ async def get_rom_content(
rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}"
files_to_download = sorted(files or [r["filename"] for r in rom.files])
log.info(f"User {current_username} is downloading {rom.file_name}")
if not rom.multi:
return FileRedirectResponse(
download_path=Path(f"/library/{rom.full_path}"),

View File

@@ -2,6 +2,7 @@ import logging
import os
from colorama import Fore, Style, init
from config import FORCE_COLOR, NO_COLOR
RED = Fore.RED
GREEN = Fore.GREEN
@@ -10,12 +11,12 @@ YELLOW = Fore.YELLOW
BLUE = Fore.BLUE
def should_strip_ansi():
def should_strip_ansi() -> bool:
"""Determine if ANSI escape codes should be stripped."""
# Check if an explicit environment variable is set to control color behavior
if os.getenv("FORCE_COLOR", "false").lower() == "true":
if FORCE_COLOR:
return False
if os.getenv("NO_COLOR", "false").lower() == "true":
if NO_COLOR:
return True
# For other environments, strip colors if not a TTY
@@ -31,7 +32,7 @@ class Formatter(logging.Formatter):
Logger formatter.
"""
def format(self, record):
def format(self, record: logging.LogRecord) -> str:
"""
Formats a log record with color-coded output based on the log level.
@@ -41,8 +42,8 @@ class Formatter(logging.Formatter):
Returns:
The formatted log record as a string.
"""
level: str = "%(levelname)s"
dots: str = f"{Fore.RESET}:"
level = "%(levelname)s"
dots = f"{Fore.RESET}:"
identifier = (
f"\t {Fore.BLUE}[RomM]{Fore.LIGHTMAGENTA_EX}[{str(record.module_name).lower()}]"
if hasattr(record, "module_name")
@@ -58,9 +59,9 @@ class Formatter(logging.Formatter):
if hasattr(record, "module_name")
else f" {Fore.BLUE}[RomM]{Fore.LIGHTMAGENTA_EX}[%(module)s]"
)
msg: str = f"{Style.RESET_ALL}%(message)s"
date: str = f"{Fore.CYAN}[%(asctime)s] "
formats: dict = {
msg = f"{Style.RESET_ALL}%(message)s"
date = f"{Fore.CYAN}[%(asctime)s] "
formats = {
logging.DEBUG: f"{Fore.LIGHTMAGENTA_EX}{level}{dots}{identifier}{date}{msg}",
logging.INFO: f"{Fore.GREEN}{level}{dots}{identifier}{date}{msg}",
logging.WARNING: f"{Fore.YELLOW}{level}{dots}{identifier_warning}{date}{msg}",

View File

@@ -120,9 +120,12 @@ class Rom(BaseModel):
@cached_property
def merged_screenshots(self) -> list[str]:
return [s.download_path for s in self.screenshots] + [
f"{FRONTEND_RESOURCES_PATH}/{s}" for s in self.path_screenshots
]
screenshots = [s.download_path for s in self.screenshots]
if self.path_screenshots:
screenshots += [
f"{FRONTEND_RESOURCES_PATH}/{s}" for s in self.path_screenshots
]
return screenshots
def get_collections(self) -> list[Collection]:
from handler.database import db_rom_handler
@@ -132,47 +135,61 @@ class Rom(BaseModel):
# Metadata fields
@property
def youtube_video_id(self) -> str:
return self.igdb_metadata.get("youtube_video_id", "")
if self.igdb_metadata:
return self.igdb_metadata.get("youtube_video_id", "")
return ""
@property
def alternative_names(self) -> list[str]:
return (
self.igdb_metadata.get("alternative_names", None)
or self.moby_metadata.get("alternate_titles", None)
(self.igdb_metadata or {}).get("alternative_names", None)
or (self.moby_metadata or {}).get("alternate_titles", None)
or []
)
@property
def first_release_date(self) -> int:
return self.igdb_metadata.get("first_release_date", 0)
if self.igdb_metadata:
return self.igdb_metadata.get("first_release_date", 0)
return 0
@property
def genres(self) -> list[str]:
return (
self.igdb_metadata.get("genres", None)
or self.moby_metadata.get("genres", None)
(self.igdb_metadata or {}).get("genres", None)
or (self.moby_metadata or {}).get("genres", None)
or []
)
@property
def franchises(self) -> list[str]:
return self.igdb_metadata.get("franchises", [])
if self.igdb_metadata:
return self.igdb_metadata.get("franchises", [])
return []
@property
def collections(self) -> list[str]:
return self.igdb_metadata.get("collections", [])
if self.igdb_metadata:
return self.igdb_metadata.get("collections", [])
return []
@property
def companies(self) -> list[str]:
return self.igdb_metadata.get("companies", [])
if self.igdb_metadata:
return self.igdb_metadata.get("companies", [])
return []
@property
def game_modes(self) -> list[str]:
return self.igdb_metadata.get("game_modes", [])
if self.igdb_metadata:
return self.igdb_metadata.get("game_modes", [])
return []
@property
def age_ratings(self) -> list[str]:
return [r["rating"] for r in self.igdb_metadata.get("age_ratings", [])]
if self.igdb_metadata:
return [r["rating"] for r in self.igdb_metadata.get("age_ratings", [])]
return []
@property
def fs_resources_path(self) -> str:

View File

@@ -152,5 +152,5 @@ VOLUME ["/romm/resources", "/romm/library", "/romm/assets", "/romm/config", "/re
EXPOSE 8080 6379/tcp
WORKDIR /romm
ENTRYPOINT ["/sbin/tini", "--"]
ENTRYPOINT ["/sbin/tini", "--", "/docker-entrypoint.sh"]
CMD ["/init"]

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Load environment variables from variants with a _FILE suffix.
# The following logic reads variables with a _FILE suffix and
# loads the contents of the file specified in the variable
# into the variable without the suffix.
for var_name in $(printenv | cut -d= -f1 | grep "_FILE$" || true); do
# If variable is empty, skip.
if [[ -z ${!var_name} ]]; then
continue
fi
var_name_no_suffix=${var_name%"_FILE"}
# If the variable without the suffix is already set, raise an error.
if [[ -n ${!var_name_no_suffix} ]]; then
echo "ERROR: Both ${var_name_no_suffix} and ${var_name} are set (but are exclusive)" >&2
exit 1
fi
file_path="${!var_name}"
# If file does not exist, raise an error.
if [[ ! -f ${file_path} ]]; then
echo "ERROR: File ${file_path} from ${var_name} does not exist" >&2
exit 1
fi
echo "Setting ${var_name_no_suffix} from ${var_name} at ${file_path}"
export "${var_name_no_suffix}"="$(cat "${file_path}")"
# Unset the _FILE variable.
unset "${var_name}"
done
exec "$@"

View File

@@ -1,48 +0,0 @@
<script setup lang="ts">
import Notification from "@/components/common/Notification.vue";
import UploadInProgress from "@/components/common/UploadInProgress.vue";
import api from "@/services/api/index";
import userApi from "@/services/api/user";
import storeAuth from "@/stores/auth";
import storeConfig from "@/stores/config";
import storeHeartbeat from "@/stores/heartbeat";
import { onBeforeMount } from "vue";
import router from "./plugins/router";
// Props
const heartbeat = storeHeartbeat();
const auth = storeAuth();
const configStore = storeConfig();
onBeforeMount(() => {
api.get("/heartbeat").then(async ({ data: data }) => {
heartbeat.set(data);
if (heartbeat.value.SHOW_SETUP_WIZARD) {
router.push({ name: "setup" });
} else {
await userApi
.fetchCurrentUser()
.then(({ data: user }) => {
auth.setUser(user);
})
.catch((error) => {
console.error(error);
});
await api.get("/config").then(({ data: data }) => {
configStore.set(data);
});
}
});
});
</script>
<template>
<v-app>
<v-main>
<notification />
<upload-in-progress />
<router-view />
</v-main>
</v-app>
</template>

7
frontend/src/RomM.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<v-app>
<v-main class="h-100">
<router-view />
</v-main>
</v-app>
</template>

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
import AdminMenu from "@/components/common/Game/AdminMenu.vue";
import CopyRomDownloadLinkDialog from "@/components/common/Game/Dialog/CopyDownloadLink.vue";
import romApi from "@/services/api/rom";
import storeDownload from "@/stores/download";
import type { DetailedRom } from "@/stores/roms";
import storeHeartbeat from "@/stores/heartbeat";
import type { DetailedRom } from "@/stores/roms";
import type { Events } from "@/types/emitter";
import {
getDownloadLink,
@@ -11,7 +12,7 @@ import {
isRuffleEmulationSupported,
} from "@/utils";
import type { Emitter } from "mitt";
import { inject, ref, computed } from "vue";
import { computed, inject, ref } from "vue";
// Props
const props = defineProps<{ rom: DetailedRom }>();
@@ -54,67 +55,71 @@ async function copyDownloadLink(rom: DetailedRom) {
</script>
<template>
<v-btn-group divided density="compact" rounded="0" class="d-flex flex-row">
<v-btn
class="flex-grow-1"
:disabled="downloadStore.value.includes(rom.id)"
@click="
romApi.downloadRom({
rom,
files: downloadStore.filesToDownload,
})
"
>
<v-tooltip
activator="parent"
location="top"
transition="fade-transition"
open-delay="1000"
>Download game</v-tooltip
<div>
<v-btn-group divided density="compact" rounded="0" class="d-flex flex-row">
<v-btn
class="flex-grow-1"
:disabled="downloadStore.value.includes(rom.id)"
@click="
romApi.downloadRom({
rom,
files: downloadStore.filesToDownload,
})
"
>
<v-icon icon="mdi-download" size="large" />
</v-btn>
<v-btn class="flex-grow-1" @click="copyDownloadLink(rom)">
<v-tooltip
activator="parent"
location="top"
transition="fade-transition"
open-delay="1000"
>Copy download link</v-tooltip
<v-tooltip
activator="parent"
location="top"
transition="fade-transition"
open-delay="1000"
>Download game</v-tooltip
>
<v-icon icon="mdi-download" size="large" />
</v-btn>
<v-btn class="flex-grow-1" @click="copyDownloadLink(rom)">
<v-tooltip
activator="parent"
location="top"
transition="fade-transition"
open-delay="1000"
>Copy download link</v-tooltip
>
<v-icon icon="mdi-content-copy" />
</v-btn>
<v-btn
v-if="ejsEmulationSupported"
class="flex-grow-1"
@click="
$router.push({
name: 'emulatorjs',
params: { rom: rom?.id },
})
"
>
<v-icon icon="mdi-content-copy" />
</v-btn>
<v-btn
v-if="ejsEmulationSupported"
class="flex-grow-1"
@click="
$router.push({
name: 'emulatorjs',
params: { rom: rom?.id },
})
"
>
<v-icon :icon="playInfoIcon" />
</v-btn>
<v-btn
v-if="ruffleEmulationSupported"
class="flex-grow-1"
@click="
$router.push({
name: 'ruffle',
params: { rom: rom?.id },
})
"
>
<v-icon :icon="playInfoIcon" />
</v-btn>
<v-menu location="bottom">
<template #activator="{ props: menuProps }">
<v-btn class="flex-grow-1" v-bind="menuProps">
<v-icon icon="mdi-dots-vertical" size="large" />
</v-btn>
</template>
<admin-menu :rom="rom" />
</v-menu>
</v-btn-group>
<v-icon :icon="playInfoIcon" />
</v-btn>
<v-btn
v-if="ruffleEmulationSupported"
class="flex-grow-1"
@click="
$router.push({
name: 'ruffle',
params: { rom: rom?.id },
})
"
>
<v-icon :icon="playInfoIcon" />
</v-btn>
<v-menu location="bottom">
<template #activator="{ props: menuProps }">
<v-btn class="flex-grow-1" v-bind="menuProps">
<v-icon icon="mdi-dots-vertical" size="large" />
</v-btn>
</template>
<admin-menu :rom="rom" />
</v-menu>
</v-btn-group>
<copy-rom-download-link-dialog />
</div>
</template>

View File

@@ -11,27 +11,8 @@ const combined = computed(() => [
</script>
<template>
<v-row no-gutters>
<v-col
class="pa-0"
cols="4"
sm="3"
lg="6"
xxl="4"
v-for="expansion in combined"
>
<a
style="text-decoration: none; color: inherit"
:href="`https://www.igdb.com/games/${expansion.slug}`"
target="_blank"
>
<related-card :game="expansion" />
</a>
<v-col cols="4" sm="3" lg="6" class="pa-1" v-for="expansion in combined">
<related-card :game="expansion" />
</v-col>
</v-row>
</template>
<style scoped>
.chip-type {
top: -0.1rem;
left: -0.1rem;
}
</style>

View File

@@ -1,9 +1,7 @@
<script setup lang="ts">
import VersionSwitcher from "@/components/Details/VersionSwitcher.vue";
import RAvatar from "@/components/common/Collection/RAvatar.vue";
import romApi from "@/services/api/rom";
import storeAuth from "@/stores/auth";
import type { Collection } from "@/stores/collections";
import storeDownload from "@/stores/download";
import type { DetailedRom } from "@/stores/roms";
import { formatBytes } from "@/utils";
@@ -12,14 +10,14 @@ import { ref, watch } from "vue";
// Props
const props = defineProps<{ rom: DetailedRom }>();
const downloadStore = storeDownload();
const auth = storeAuth();
const romUser = ref(props.rom.rom_user);
const romInfo = ref([
{ label: "SHA-1", value: props.rom.sha1_hash },
{ label: "MD5", value: props.rom.md5_hash },
{ label: "CRC", value: props.rom.crc_hash },
]);
// Functions
function collectionsWithoutFavourites(collections: Collection[]) {
return collections.filter((c) => c.name.toLowerCase() != "favourites");
}
async function toggleMainSibling() {
romUser.value.is_main_sibling = !romUser.value.is_main_sibling;
romApi.updateUserRomProps({
@@ -111,33 +109,26 @@ watch(
<span>Info</span>
</v-col>
<v-col class="my-1">
<v-chip size="small" label>
Size: {{ formatBytes(rom.file_size_bytes) }}
</v-chip>
<v-chip
v-if="!rom.multi && rom.sha1_hash"
size="small"
label
class="ml-1"
>
SHA-1: {{ rom.sha1_hash }}
</v-chip>
<v-chip
v-if="!rom.multi && rom.md5_hash"
size="small"
label
class="ml-1"
>
MD5: {{ rom.md5_hash }}
</v-chip>
<v-chip
v-if="!rom.multi && rom.crc_hash"
size="small"
label
class="ml-1"
>
CRC: {{ rom.crc_hash }}
</v-chip>
<v-row no-gutters>
<v-col cols="12">
<v-chip size="small" class="mr-2 px-0" label>
<v-chip label>Size</v-chip
><span class="px-2">{{
formatBytes(rom.file_size_bytes)
}}</span>
</v-chip>
</v-col>
<v-col
v-for="info in romInfo"
v-if="!rom.multi && rom.sha1_hash"
cols="12"
>
<v-chip size="small" class="mt-1 mr-2 px-0" label>
<v-chip label>{{ info.label }}</v-chip
><span class="px-2">{{ info.value }}</span>
</v-chip>
</v-col>
</v-row>
</v-col>
</v-row>
<v-row v-if="rom.tags.length > 0" class="align-center my-3" no-gutters>
@@ -158,34 +149,6 @@ watch(
</v-chip>
</v-col>
</v-row>
<v-row
v-if="
rom.user_collections &&
collectionsWithoutFavourites(rom.user_collections).length > 0
"
no-gutters
class="align-center my-3"
>
<v-col cols="3" xl="2">
<span>Collections</span>
</v-col>
<v-col>
<v-chip
v-for="collection in collectionsWithoutFavourites(
rom.user_collections,
)"
:to="{ name: 'collection', params: { collection: collection.id } }"
size="large"
class="mr-1 mt-1"
label
>
<template #prepend>
<r-avatar :size="25" :collection="collection" />
</template>
<span class="ml-2">{{ collection.name }}</span>
</v-chip>
</v-col>
</v-row>
</v-col>
</v-row>
</template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { type FilterType } from "@/stores/galleryFilter";
import storeGalleryView from "@/stores/galleryView";
import RAvatar from "@/components/common/Collection/RAvatar.vue";
import type { DetailedRom } from "@/stores/roms";
import { storeToRefs } from "pinia";
import { ref } from "vue";
@@ -30,6 +31,35 @@ function onFilterClick(filter: FilterType, value: string) {
<v-row no-gutters>
<v-col>
<v-divider class="mx-2 my-4" />
<v-row
v-if="rom.user_collections && rom.user_collections.length > 0"
no-gutters
class="align-center my-3"
>
<v-col cols="3" xl="2">
<span>RomM Collections</span>
</v-col>
<v-col>
<v-row no-gutters>
<v-col cols="12" v-for="collection in rom.user_collections">
<v-chip
:to="{
name: 'collection',
params: { collection: collection.id },
}"
size="large"
class="mr-1 mt-1 px-0"
label
>
<template #prepend>
<r-avatar :size="38" :collection="collection" />
</template>
<span class="px-4">{{ collection.name }}</span>
</v-chip>
</v-col>
</v-row>
</v-col>
</v-row>
<template v-for="filter in filters" :key="filter">
<v-row
v-if="rom[filter].length > 0"

View File

@@ -76,7 +76,7 @@ watch(
color="romm-accent-1"
hide-details
>
<template v-slot:label
<template #label
><span>Backlogged</span
><span class="ml-2">{{
getEmojiForStatus("backlogged")
@@ -88,7 +88,7 @@ watch(
color="romm-accent-1"
hide-details
>
<template v-slot:label
<template #label
><span>Now playing</span
><span class="ml-2">{{
getEmojiForStatus("now_playing")
@@ -100,7 +100,7 @@ watch(
color="romm-accent-1"
hide-details
>
<template v-slot:label
<template #label
><span>Hidden</span
><span class="ml-2">{{
getEmojiForStatus("hidden")
@@ -190,13 +190,13 @@ watch(
density="compact"
class="mt-1"
>
<template v-slot:selection="{ item }">
<template #selection="{ item }">
<span>{{ getEmojiForStatus(item.raw as RomUserStatus) }}</span
><span class="ml-2">{{
getTextForStatus(item.raw as RomUserStatus)
}}</span>
</template>
<template v-slot:item="{ item }">
<template #item="{ item }">
<v-list-item
link
rounded="0"

View File

@@ -4,7 +4,7 @@ import type { DetailedRom } from "@/stores/roms";
import { computed } from "vue";
const props = defineProps<{ rom: DetailedRom }>();
const games = computed(() => [
const combined = computed(() => [
...(props.rom.igdb_metadata?.remakes ?? []),
...(props.rom.igdb_metadata?.remasters ?? []),
...(props.rom.igdb_metadata?.expanded_games ?? []),
@@ -12,7 +12,7 @@ const games = computed(() => [
</script>
<template>
<v-row no-gutters>
<v-col cols="4" sm="3" md="6" v-for="game in games">
<v-col cols="4" sm="3" md="6" class="px-1" v-for="game in combined">
<related-card :game="game" />
</v-col>
</v-row>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { SaveSchema } from "@/__generated__";
import DeleteAssetDialog from "@/components/common/Game/Dialog/Asset/DeleteAssets.vue";
import UploadSavesDialog from "@/components/common/Game/Dialog/Asset/UploadSaves.vue";
import { type DetailedRom } from "@/stores/roms";
import type { Events } from "@/types/emitter";
@@ -196,6 +197,7 @@ onMounted(() => {
</template>
</v-data-table>
<upload-saves-dialog />
<delete-asset-dialog />
</template>
<style scoped>
.name-row {

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { StateSchema } from "@/__generated__";
import DeleteAssetDialog from "@/components/common/Game/Dialog/Asset/DeleteAssets.vue";
import UploadStatesDialog from "@/components/common/Game/Dialog/Asset/UploadStates.vue";
import { type DetailedRom } from "@/stores/roms";
import type { Events } from "@/types/emitter";
@@ -197,6 +198,7 @@ onMounted(() => {
</template>
</v-data-table>
<upload-states-dialog />
<delete-asset-dialog />
</template>
<style scoped>
.name-row {

View File

@@ -20,50 +20,94 @@ const releaseDate = new Date(
const hasReleaseDate = Number(props.rom.first_release_date) > 0;
</script>
<template>
<v-row
class="text-white text-shadow"
:class="{ 'text-center mt-2': smAndDown }"
no-gutters
>
<v-col>
<v-list-item class="text-h5 font-weight-bold pl-0">
<span>{{ rom.name }}</span>
<fav-btn class="ml-1" :rom="rom" />
</v-list-item>
</v-col>
</v-row>
<div>
<v-row
class="text-white text-shadow"
:class="{ 'text-center my-4': smAndDown }"
no-gutters
>
<v-col>
<p class="text-h5 font-weight-bold pl-0">
<span>{{ rom.name }}</span>
<fav-btn class="ml-2" :rom="rom" />
</p>
</v-col>
</v-row>
<v-row
class="text-white text-shadow mt-2"
:class="{ 'text-center': smAndDown }"
no-gutters
>
<v-col>
<v-chip :to="{ name: 'platform', params: { platform: rom.platform_id } }">
{{ rom.platform_name }}
<platform-icon
:key="rom.platform_slug"
:slug="rom.platform_slug"
:name="rom.platform_name"
:size="30"
class="ml-2"
/>
</v-chip>
<v-chip
v-if="hasReleaseDate && !smAndDown"
class="ml-1 font-italic"
size="x-small"
>
{{ releaseDate }}
</v-chip>
<v-chip
v-if="Number(rom.first_release_date) > 0 && smAndDown"
class="font-italic ml-1"
size="x-small"
>
{{ releaseDate }}
</v-chip>
<template v-if="!smAndDown">
<v-row
class="text-white text-shadow mt-2"
:class="{ 'text-center': smAndDown }"
no-gutters
>
<v-col>
<v-chip
:to="{ name: 'platform', params: { platform: rom.platform_id } }"
>
{{ rom.platform_name }}
<platform-icon
:key="rom.platform_slug"
:slug="rom.platform_slug"
:name="rom.platform_name"
:size="30"
class="ml-2"
/>
</v-chip>
<v-chip
v-if="hasReleaseDate && !smAndDown"
class="ml-1 font-italic"
size="x-small"
>
{{ releaseDate }}
</v-chip>
<v-chip
v-if="Number(rom.first_release_date) > 0 && smAndDown"
class="font-italic ml-1"
size="x-small"
>
{{ releaseDate }}
</v-chip>
<template v-if="!smAndDown">
<v-chip
class="ml-1"
v-if="rom.regions.filter(identity).length > 0"
size="small"
:title="`Regions: ${rom.regions.join(', ')}`"
>
<span v-for="region in rom.regions" :key="region" class="px-1">{{
regionToEmoji(region)
}}</span>
</v-chip>
<v-chip
v-if="rom.languages.filter(identity).length > 0"
size="small"
class="ml-1"
:title="`Languages: ${rom.languages.join(', ')}`"
>
<span
v-for="language in rom.languages"
:key="language"
class="px-1"
>{{ languageToEmoji(language) }}</span
>
</v-chip>
<v-chip v-if="rom.revision" size="small" class="ml-1">
Revision {{ rom.revision }}
</v-chip>
</template>
</v-col>
</v-row>
<v-row
v-if="
smAndDown &&
rom.regions.filter(identity).length > 0 &&
rom.languages.filter(identity).length > 0 &&
rom.revision
"
class="text-white text-shadow mt-2 text-center"
no-gutters
>
<v-col>
<v-chip
class="ml-1"
v-if="rom.regions.filter(identity).length > 0"
@@ -90,83 +134,46 @@ const hasReleaseDate = Number(props.rom.first_release_date) > 0;
<v-chip v-if="rom.revision" size="small" class="ml-1">
Revision {{ rom.revision }}
</v-chip>
</template>
</v-col>
</v-row>
</v-col>
</v-row>
<v-row
v-if="
smAndDown &&
rom.regions.filter(identity).length > 0 &&
rom.languages.filter(identity).length > 0 &&
rom.revision
"
class="text-white text-shadow mt-2 text-center"
no-gutters
>
<v-col>
<v-chip
class="ml-1"
v-if="rom.regions.filter(identity).length > 0"
size="small"
:title="`Regions: ${rom.regions.join(', ')}`"
>
<span v-for="region in rom.regions" :key="region" class="px-1">{{
regionToEmoji(region)
}}</span>
</v-chip>
<v-chip
v-if="rom.languages.filter(identity).length > 0"
size="small"
class="ml-1"
:title="`Languages: ${rom.languages.join(', ')}`"
>
<span v-for="language in rom.languages" :key="language" class="px-1">{{
languageToEmoji(language)
}}</span>
</v-chip>
<v-chip v-if="rom.revision" size="small" class="ml-1">
Revision {{ rom.revision }}
</v-chip>
</v-col>
</v-row>
<v-row
v-if="rom.igdb_id || rom.moby_id"
class="text-white text-shadow mt-2"
:class="{ 'text-center': smAndDown }"
no-gutters
>
<v-col cols="12">
<a
v-if="rom.igdb_id"
style="text-decoration: none; color: inherit"
:href="`https://www.igdb.com/games/${rom.slug}`"
target="_blank"
>
<v-chip size="x-small" @click.stop>
<span>IGDB</span>
<v-divider class="mx-2 border-opacity-25" vertical />
<span>ID: {{ rom.igdb_id }}</span>
<v-divider class="mx-2 border-opacity-25" vertical />
<span>Rating: {{ rom.igdb_metadata?.total_rating }}</span>
</v-chip>
</a>
<a
v-if="rom.moby_id"
style="text-decoration: none; color: inherit"
:href="`https://www.mobygames.com/game/${rom.moby_id}`"
target="_blank"
:class="{ 'ml-1': rom.igdb_id }"
>
<v-chip size="x-small" @click.stop>
<span>Mobygames</span>
<v-divider class="mx-2 border-opacity-25" vertical />
<span>ID: {{ rom.moby_id }}</span>
<v-divider class="mx-2 border-opacity-25" vertical />
<span>Rating: {{ rom.moby_metadata?.moby_score }}</span>
</v-chip>
</a>
</v-col>
</v-row>
<v-row
v-if="rom.igdb_id || rom.moby_id"
class="text-white text-shadow mt-2"
:class="{ 'text-center': smAndDown }"
no-gutters
>
<v-col cols="12">
<a
v-if="rom.igdb_id"
style="text-decoration: none; color: inherit"
:href="`https://www.igdb.com/games/${rom.slug}`"
target="_blank"
>
<v-chip size="x-small" @click.stop>
<span>IGDB</span>
<v-divider class="mx-2 border-opacity-25" vertical />
<span>ID: {{ rom.igdb_id }}</span>
<v-divider class="mx-2 border-opacity-25" vertical />
<span>Rating: {{ rom.igdb_metadata?.total_rating }}</span>
</v-chip>
</a>
<a
v-if="rom.moby_id"
style="text-decoration: none; color: inherit"
:href="`https://www.mobygames.com/game/${rom.moby_id}`"
target="_blank"
:class="{ 'ml-1': rom.igdb_id }"
>
<v-chip size="x-small" @click.stop>
<span>Mobygames</span>
<v-divider class="mx-2 border-opacity-25" vertical />
<span>ID: {{ rom.moby_id }}</span>
<v-divider class="mx-2 border-opacity-25" vertical />
<span>Rating: {{ rom.moby_metadata?.moby_score }}</span>
</v-chip>
</a>
</v-col>
</v-row>
</div>
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import FilterBtn from "@/components/Gallery/AppBar/common/FilterBtn.vue";
import CollectionInfoDrawer from "@/components/Gallery/AppBar/Collection/CollectionInfoDrawer.vue";
import FilterDrawer from "@/components/Gallery/AppBar/common/FilterDrawer/Base.vue";
import FilterTextField from "@/components/Gallery/AppBar/common/FilterTextField.vue";
import GalleryViewBtn from "@/components/Gallery/AppBar/common/GalleryViewBtn.vue";
@@ -11,6 +12,7 @@ import storeRoms from "@/stores/roms";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { storeToRefs } from "pinia";
import storeNavigation from "@/stores/navigation";
import { inject, ref } from "vue";
import { useDisplay } from "vuetify";
@@ -21,29 +23,9 @@ const viewportWidth = ref(window.innerWidth);
const auth = storeAuth();
const romsStore = storeRoms();
const { currentCollection } = storeToRefs(romsStore);
const navigationStore = storeNavigation();
const { activeCollectionInfoDrawer } = storeToRefs(navigationStore);
const open = ref(false);
const collectionInfoFields = [
{
key: "name",
label: "Name",
},
{
key: "description",
label: "Description",
},
{
key: "rom_count",
label: "Roms",
},
{
key: "user__username",
label: "Owner",
},
{
key: "is_public",
label: "Public",
},
];
</script>
<template>
@@ -58,9 +40,9 @@ const collectionInfoFields = [
>
<template #prepend>
<v-btn
:color="open ? 'romm-accent-1' : ''"
:color="activeCollectionInfoDrawer ? 'romm-accent-1' : ''"
rounded="0"
@click="open = !open"
@click="navigationStore.swtichActiveCollectionInfoDrawer"
icon="mdi-information"
></v-btn>
<filter-btn />
@@ -72,91 +54,7 @@ const collectionInfoFields = [
</template>
</v-app-bar>
<v-navigation-drawer
v-model="open"
floating
mobile
:width="xs ? viewportWidth : '500'"
v-if="currentCollection"
>
<v-row no-gutters class="text-center justify-center align-center mt-4">
<v-col style="max-width: 240px">
<collection-card
:key="currentCollection.updated_at"
:collection="currentCollection"
/>
<v-btn
rounded="4"
@click="
emitter?.emit('showEditCollectionDialog', { ...currentCollection })
"
class="mt-4 bg-terciary"
>
<template #prepend>
<v-icon>mdi-pencil-box</v-icon>
</template>
Edit collection
</v-btn>
</v-col>
</v-row>
<v-row no-gutters class="mt-4">
<v-col cols="12">
<v-card class="mx-4 bg-terciary" elevation="0">
<v-card-text class="pa-4">
<template
v-for="(field, index) in collectionInfoFields"
:key="field.key"
>
<div
v-if="
currentCollection[field.key as keyof typeof currentCollection]
"
:class="{ 'mt-4': index !== 0 }"
>
<p class="text-subtitle-1 text-decoration-underline">
{{ field.label }}
</p>
<p class="text-subtitle-2">
{{
currentCollection[
field.key as keyof typeof currentCollection
]
}}
</p>
</div>
</template>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row no-gutters class="mt-4">
<v-col cols="12">
<r-section
v-if="auth.scopes.includes('collections.write')"
icon="mdi-alert"
icon-color="red"
title="Danger zone"
elevation="0"
>
<template #content>
<div class="text-center my-2">
<v-btn
class="text-romm-red bg-terciary"
variant="flat"
@click="
emitter?.emit('showDeleteCollectionDialog', currentCollection)
"
>
<v-icon class="text-romm-red mr-2">mdi-delete</v-icon>
Delete collection
</v-btn>
</div>
</template>
</r-section>
</v-col>
</v-row>
</v-navigation-drawer>
<collection-info-drawer />
<filter-drawer />
</template>

View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
import CollectionCard from "@/components/common/Collection/Card.vue";
import DeleteCollectionDialog from "@/components/common/Collection/Dialog/DeleteCollection.vue";
import EditCollectionDialog from "@/components/common/Collection/Dialog/EditCollection.vue";
import RSection from "@/components/common/RSection.vue";
import storeAuth from "@/stores/auth";
import storeNavigation from "@/stores/navigation";
import storeRoms from "@/stores/roms";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { storeToRefs } from "pinia";
import { inject, ref } from "vue";
import { useDisplay } from "vuetify";
// Props
const { xs } = useDisplay();
const emitter = inject<Emitter<Events>>("emitter");
const viewportWidth = ref(window.innerWidth);
const auth = storeAuth();
const romsStore = storeRoms();
const { currentCollection } = storeToRefs(romsStore);
const navigationStore = storeNavigation();
const { activeCollectionInfoDrawer } = storeToRefs(navigationStore);
const collectionInfoFields = [
{
key: "rom_count",
label: "Roms",
},
{
key: "user__username",
label: "Owner",
},
{
key: "is_public",
label: "Public",
},
];
</script>
<template>
<v-navigation-drawer
v-model="activeCollectionInfoDrawer"
floating
mobile
:width="xs ? viewportWidth : '500'"
v-if="currentCollection"
>
<v-row no-gutters class="justify-center align-center pa-4">
<v-col style="max-width: 240px" cols="12">
<div class="text-center justify-center align-center">
<collection-card
:key="currentCollection.updated_at"
:collection="currentCollection"
/>
</div>
<div
class="text-center mt-4"
v-if="auth.scopes.includes('collections.write')"
>
<p class="text-h5 font-weight-bold pl-0">
<span>{{ currentCollection.name }}</span>
</p>
<p class="text-subtitle-2">
<span>{{ currentCollection.description }}</span>
</p>
<div class="mt-6">
<v-btn
v-if="currentCollection.user__username === auth.user?.username"
rounded="4"
@click="
emitter?.emit('showEditCollectionDialog', {
...currentCollection,
})
"
class="bg-terciary"
>
<template #prepend>
<v-icon>mdi-pencil-box</v-icon>
</template>
Edit collection
</v-btn>
</div>
</div>
</v-col>
<v-col cols="12">
<v-card class="mt-4 bg-terciary fill-width" elevation="0">
<v-card-text class="pa-4">
<template
v-for="(field, index) in collectionInfoFields"
:key="field.key"
>
<div
v-if="
currentCollection[field.key as keyof typeof currentCollection]
"
:class="{ 'mt-4': index !== 0 }"
>
<p class="text-subtitle-1 text-decoration-underline">
{{ field.label }}
</p>
<p class="text-subtitle-2">
{{
currentCollection[
field.key as keyof typeof currentCollection
]
}}
</p>
</div>
</template>
</v-card-text>
</v-card>
</v-col>
</v-row>
<r-section
v-if="
auth.scopes.includes('collections.write') &&
currentCollection.user__username === auth.user?.username
"
icon="mdi-alert"
icon-color="red"
title="Danger zone"
elevation="0"
>
<template #content>
<div class="text-center">
<v-btn
class="text-romm-red bg-terciary ma-2"
variant="flat"
@click="
emitter?.emit('showDeleteCollectionDialog', currentCollection)
"
>
<v-icon class="text-romm-red mr-2">mdi-delete</v-icon>
Delete collection
</v-btn>
</div>
</template>
</r-section>
</v-navigation-drawer>
<edit-collection-dialog />
<delete-collection-dialog />
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import DeletePlatformDialog from "@/components/common/Platform/Dialog/DeletePlatform.vue";
import PlatformIcon from "@/components/common/Platform/Icon.vue";
import RSection from "@/components/common/RSection.vue";
import platformApi from "@/services/api/platform";
@@ -46,7 +47,6 @@ const aspectRatioOptions = computed(() => [
]);
const platformInfoFields = [
{ key: "name", label: "Name" },
{ key: "slug", label: "Slug" },
{ key: "fs_slug", label: "Filesystem folder name" },
{ key: "category", label: "Category" },
@@ -125,9 +125,9 @@ async function setAspectRatio() {
:width="xs ? viewportWidth : '500'"
v-if="currentPlatform"
>
<v-row no-gutters class="justify-center align-center">
<v-row no-gutters class="justify-center align-center pa-4">
<v-col cols="12">
<div class="text-center mt-2">
<div class="text-center justify-center align-center">
<platform-icon
:slug="currentPlatform.slug"
:name="currentPlatform.name"
@@ -136,38 +136,43 @@ async function setAspectRatio() {
/>
</div>
<div
class="text-center mt-4"
class="text-center mt-2"
v-if="auth.scopes.includes('platforms.write')"
>
<v-btn
class="bg-terciary"
@click="emitter?.emit('showUploadRomDialog', currentPlatform)"
>
<v-icon class="text-romm-green mr-2">mdi-upload</v-icon>
Upload roms
</v-btn>
<v-btn
:disabled="scanning"
rounded="4"
:loading="scanning"
@click="scan"
class="ml-2 bg-terciary"
>
<template #prepend>
<v-icon :color="scanning ? '' : 'romm-accent-1'"
>mdi-magnify-scan</v-icon
>
</template>
Scan platform
<template #loader>
<v-progress-circular
color="romm-accent-1"
:width="2"
:size="20"
indeterminate
/>
</template>
</v-btn>
<p class="text-h5 font-weight-bold pl-0">
<span>{{ currentPlatform.name }}</span>
</p>
<div class="mt-6">
<v-btn
class="bg-terciary"
@click="emitter?.emit('showUploadRomDialog', currentPlatform)"
>
<v-icon class="text-romm-green mr-2">mdi-upload</v-icon>
Upload roms
</v-btn>
<v-btn
:disabled="scanning"
rounded="4"
:loading="scanning"
@click="scan"
class="ml-2 bg-terciary"
>
<template #prepend>
<v-icon :color="scanning ? '' : 'romm-accent-1'"
>mdi-magnify-scan</v-icon
>
</template>
Scan platform
<template #loader>
<v-progress-circular
color="romm-accent-1"
:width="2"
:size="20"
indeterminate
/>
</template>
</v-btn>
</div>
</div>
<div class="mt-4 text-center">
<a
@@ -194,7 +199,7 @@ async function setAspectRatio() {
<span>ID: {{ currentPlatform.moby_id }}</span>
</v-chip>
</div>
<v-card class="mt-4 mx-4 bg-terciary fill-width" elevation="0">
<v-card class="mt-4 bg-terciary fill-width" elevation="0">
<v-card-text class="pa-4">
<template
v-for="(field, index) in platformInfoFields"
@@ -220,78 +225,85 @@ async function setAspectRatio() {
</v-card>
</v-col>
</v-row>
<v-row class="mt-4" no-gutters>
<v-col cols="12">
<r-section
v-if="auth.scopes.includes('platforms.write')"
icon="mdi-aspect-ratio"
title="UI Settings"
elevation="0"
<r-section
v-if="auth.scopes.includes('platforms.write')"
icon="mdi-cog"
title="Settings"
elevation="0"
>
<template #content>
<v-chip
label
variant="text"
class="ml-2"
prepend-icon="mdi-aspect-ratio"
>Cover style</v-chip
>
<template #content>
<v-item-group
v-model="selectedAspectRatio"
mandatory
@update:model-value="setAspectRatio"
>
<v-row no-gutters class="text-center justify-center align-center">
<v-col class="ma-2" v-for="aspectRatio in aspectRatioOptions">
<v-item v-slot="{ isSelected, toggle }">
<v-card
:color="isSelected ? 'romm-accent-1' : 'romm-gray'"
variant="outlined"
@click="toggle"
<v-divider class="border-opacity-25 mx-2" />
<v-item-group
v-model="selectedAspectRatio"
mandatory
@update:model-value="setAspectRatio"
>
<v-row
no-gutters
class="text-center justify-center align-center pa-2"
>
<v-col class="pa-2" v-for="aspectRatio in aspectRatioOptions">
<v-item v-slot="{ isSelected, toggle }">
<v-card
:color="isSelected ? 'romm-accent-1' : 'romm-gray'"
variant="outlined"
@click="toggle"
>
<v-card-text
class="pa-0 text-center align-center justify-center"
>
<v-img
:aspect-ratio="aspectRatio.size"
cover
src="/assets/login_bg.png"
:class="{ greyscale: !isSelected }"
class="d-flex align-center justify-center"
>
<v-card-text
class="pa-0 text-center align-center justify-center"
>
<v-img
:aspect-ratio="aspectRatio.size"
cover
src="/assets/login_bg.png"
:class="{ greyscale: !isSelected }"
class="d-flex align-center justify-center"
>
<p class="text-h5 text-romm-white">
{{ aspectRatio.name }}
</p>
</v-img>
<p class="text-center mx-2 text-caption">
{{ aspectRatio.source }}
</p>
</v-card-text>
</v-card>
</v-item>
</v-col>
</v-row>
</v-item-group>
</template>
</r-section>
<r-section
v-if="auth.scopes.includes('platforms.write')"
icon="mdi-alert"
icon-color="red"
title="Danger zone"
elevation="0"
>
<template #content>
<div class="text-center my-2">
<v-btn
class="text-romm-red bg-terciary"
variant="flat"
@click="
emitter?.emit('showDeletePlatformDialog', currentPlatform)
"
>
<v-icon class="text-romm-red mr-2">mdi-delete</v-icon>
Delete platform
</v-btn>
</div>
</template>
</r-section>
</v-col>
</v-row>
<p class="text-h5 text-romm-white">
{{ aspectRatio.name }}
</p>
</v-img>
<p class="text-center text-caption">
{{ aspectRatio.source }}
</p>
</v-card-text>
</v-card>
</v-item>
</v-col>
</v-row>
</v-item-group>
</template>
</r-section>
<r-section
v-if="auth.scopes.includes('platforms.write')"
icon="mdi-alert"
icon-color="red"
title="Danger zone"
elevation="0"
>
<template #content>
<div class="text-center">
<v-btn
class="text-romm-red bg-terciary ma-2"
variant="flat"
@click="emitter?.emit('showDeletePlatformDialog', currentPlatform)"
>
<v-icon class="text-romm-red mr-2">mdi-delete</v-icon>
Delete platform
</v-btn>
</div>
</template>
</r-section>
</v-navigation-drawer>
<delete-platform-dialog />
</template>
<style scoped>
.platform-icon {

View File

@@ -3,14 +3,21 @@ import CollectionCard from "@/components/common/Collection/Card.vue";
import RSection from "@/components/common/RSection.vue";
import storeCollections from "@/stores/collections";
import { views } from "@/utils";
import { isNull } from "lodash";
// Props
const collections = storeCollections();
const gridCollections = isNull(localStorage.getItem("settings.gridCollections"))
? true
: localStorage.getItem("settings.gridCollections") === "true";
</script>
<template>
<r-section icon="mdi-bookmark-box-multiple" title="Collections">
<template #content>
<v-row no-gutters>
<v-row
:class="{ 'flex-nowrap overflow-x-auto': !gridCollections }"
no-gutters
>
<v-col
v-for="collection in collections.allCollections"
:key="collection.name"
@@ -24,10 +31,10 @@ const collections = storeCollections();
<collection-card
show-rom-count
show-title
transformScale
transform-scale
:key="collection.updated_at"
:collection="collection"
withLink
with-link
/>
</v-col>
</v-row>

View File

@@ -2,15 +2,22 @@
import PlatformCard from "@/components/common/Platform/Card.vue";
import RSection from "@/components/common/RSection.vue";
import storePlatforms from "@/stores/platforms";
import { isNull } from "lodash";
import { views } from "@/utils";
// Props
const platforms = storePlatforms();
const gridPlatforms = isNull(localStorage.getItem("settings.gridPlatforms"))
? true
: localStorage.getItem("settings.gridPlatforms") === "true";
</script>
<template>
<r-section icon="mdi-controller" title="Platforms">
<template #content>
<v-row no-gutters>
<v-row
:class="{ 'flex-nowrap overflow-x-auto': !gridPlatforms }"
no-gutters
>
<v-col
v-for="platform in platforms.filledPlatforms"
:key="platform.slug"

View File

@@ -5,29 +5,23 @@ import storeRoms, { type SimpleRom } from "@/stores/roms";
import { views } from "@/utils";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { isNull } from "lodash";
// Props
const romsStore = storeRoms();
const { recentRoms } = storeToRefs(romsStore);
const router = useRouter();
// Functions
function onGameClick(emitData: { rom: SimpleRom; event: MouseEvent }) {
if (emitData.event.metaKey || emitData.event.ctrlKey) {
const link = router.resolve({
name: "rom",
params: { rom: emitData.rom.id },
});
window.open(link.href, "_blank");
} else {
router.push({ name: "rom", params: { rom: emitData.rom.id } });
}
}
const gridRecentRoms = isNull(localStorage.getItem("settings.gridRecentRoms"))
? true
: localStorage.getItem("settings.gridRecentRoms") === "true";
</script>
<template>
<r-section icon="mdi-shimmer" title="Recently added">
<template #content>
<v-row class="flex-nowrap overflow-x-auto align-center" no-gutters>
<v-row
:class="{ 'flex-nowrap overflow-x-auto': !gridRecentRoms }"
no-gutters
>
<v-col
v-for="rom in recentRoms"
:key="rom.id"
@@ -41,10 +35,9 @@ function onGameClick(emitData: { rom: SimpleRom; event: MouseEvent }) {
<game-card
:key="rom.updated_at"
:rom="rom"
@click="onGameClick"
title-on-hover
pointerOnHover
withLink
pointer-on-hover
with-link
show-flags
show-fav
transform-scale

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import Task from "@/components/Settings/TaskOption.vue";
import Task from "@/components/Settings/Administration/TaskOption.vue";
import RSection from "@/components/common/RSection.vue";
import api from "@/services/api/index";
import storeHeartbeat from "@/stores/heartbeat";
@@ -13,7 +13,6 @@ import { computed, inject } from "vue";
const emitter = inject<Emitter<Events>>("emitter");
const heartbeatStore = storeHeartbeat();
const runningTasks = storeRunningTasks();
const tasks = computed(() => [
{
title: heartbeatStore.value.WATCHER.TITLE,
@@ -47,7 +46,7 @@ const tasks = computed(() => [
},
]);
// Methods
// Functions
const runAllTasks = async () => {
runningTasks.value = true;
const result = await api.post("/tasks/run");

View File

@@ -2,7 +2,8 @@
import RDialog from "@/components/common/RDialog.vue";
import userApi from "@/services/api/user";
import storeUsers from "@/stores/users";
import type { Events, UserItem } from "@/types/emitter";
import type { Events } from "@/types/emitter";
import type { UserItem } from "@/types/user";
import { defaultAvatarPath } from "@/utils";
import type { Emitter } from "mitt";
import { inject, ref } from "vue";

View File

@@ -3,7 +3,8 @@ import RDialog from "@/components/common/RDialog.vue";
import userApi from "@/services/api/user";
import storeAuth from "@/stores/auth";
import storeUsers from "@/stores/users";
import type { Events, UserItem } from "@/types/emitter";
import type { Events } from "@/types/emitter";
import type { UserItem } from "@/types/user";
import { defaultAvatarPath } from "@/utils";
import type { Emitter } from "mitt";
import { inject, ref } from "vue";
@@ -134,17 +135,16 @@ function closeDialog() {
<v-avatar size="190" class="ml-4" v-bind="props">
<v-img
:src="
imagePreviewUrl
? imagePreviewUrl
: user.avatar_path
? `/assets/romm/assets/${user.avatar_path}?ts=${user.updated_at}`
: defaultAvatarPath
imagePreviewUrl ||
(user.avatar_path
? `/assets/romm/assets/${user.avatar_path}?ts=${user.updated_at}`
: defaultAvatarPath)
"
>
<v-fade-transition>
<div
v-if="isHovering"
class="d-flex translucent edit-hover text-h4"
class="d-flex translucent cursor-pointer h-100 align-center justify-center text-h4"
@click="triggerFileInput"
>
<v-icon>mdi-pencil</v-icon>
@@ -185,15 +185,3 @@ function closeDialog() {
</template>
</r-dialog>
</template>
<style scoped>
.edit-hover {
align-items: center;
bottom: 0;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
cursor: pointer;
}
</style>

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import CreateUserDialog from "@/components/Settings/Administration/Users/Dialog/CreateUser.vue";
import DeleteUserDialog from "@/components/Settings/Administration/Users/Dialog/DeleteUser.vue";
import RSection from "@/components/common/RSection.vue";
import userApi from "@/services/api/user";
import storeAuth from "@/stores/auth";
@@ -139,6 +141,7 @@ onMounted(() => {
</template>
<template #item.enabled="{ item }">
<v-switch
inset
v-model="item.enabled"
color="romm-accent-1"
:disabled="item.id == auth.user?.id"
@@ -194,4 +197,7 @@ onMounted(() => {
</v-data-table>
</template>
</r-section>
<create-user-dialog />
<delete-user-dialog />
</template>

View File

@@ -4,7 +4,11 @@ import storeHeartbeat from "@/stores/heartbeat";
const heartbeatStore = storeHeartbeat();
</script>
<template>
<v-bottom-navigation :elevation="0" height="36" class="text-caption">
<v-bottom-navigation
:elevation="0"
height="36"
class="bg-terciary text-caption"
>
<v-row class="align-center justify-center" no-gutters>
<span class="text-romm-accent-1">RomM</span>
<span class="ml-1">{{ heartbeatStore.value.VERSION }}</span>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import ExcludedCard from "@/components/Management/ExcludedCard.vue";
import RSection from "@/components/common/RSection.vue";
import CreateExclusionDialog from "@/components/Settings/LibraryManagement/Dialog/CreateExclusion.vue";
import ExcludedCard from "@/components/Settings/LibraryManagement/ExcludedCard.vue";
import storeAuth from "@/stores/auth";
import storeConfig from "@/stores/config";
import { ref } from "vue";
@@ -74,4 +75,6 @@ const editable = ref(false);
/>
</template>
</r-section>
<create-exclusion-dialog />
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import AddBtn from "@/components/Management/AddBtn.vue";
import PlatformBindCard from "@/components/Management/PlatformBindCard.vue";
import AddBtn from "@/components/Settings/LibraryManagement/AddBtn.vue";
import CreatePlatformBindingDialog from "@/components/Settings/LibraryManagement/Dialog/CreatePlatformBinding.vue";
import DeletePlatformBindingDialog from "@/components/Settings/LibraryManagement/Dialog/DeletePlatformBinding.vue";
import PlatformBindCard from "@/components/Settings/LibraryManagement/PlatformBindCard.vue";
import RSection from "@/components/common/RSection.vue";
import storeAuth from "@/stores/auth";
import storeConfig from "@/stores/config";
@@ -74,4 +76,7 @@ const editable = ref(false);
</v-row>
</template>
</r-section>
<create-platform-binding-dialog />
<delete-platform-binding-dialog />
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import AddBtn from "@/components/Management/AddBtn.vue";
import PlatformBindCard from "@/components/Management/PlatformBindCard.vue";
import AddBtn from "@/components/Settings/LibraryManagement/AddBtn.vue";
import CreatePlatformVersionDialog from "@/components/Settings/LibraryManagement/Dialog/CreatePlatformVersion.vue";
import DeletePlatformVersionDialog from "@/components/Settings/LibraryManagement/Dialog/DeletePlatformVersion.vue";
import PlatformBindCard from "@/components/Settings/LibraryManagement/PlatformBindCard.vue";
import RSection from "@/components/common/RSection.vue";
import storeAuth from "@/stores/auth";
import storeConfig from "@/stores/config";
@@ -74,4 +76,7 @@ const editable = ref(false);
</v-row>
</template>
</r-section>
<create-platform-version-dialog />
<delete-platform-version-dialog />
</template>

View File

@@ -0,0 +1,258 @@
<script setup lang="ts">
import InterfaceOption from "@/components/Settings/UserInterface/InterfaceOption.vue";
import RSection from "@/components/common/RSection.vue";
import { computed, ref } from "vue";
import { isNull } from "lodash";
// Initializing refs from localStorage
const storedShowRecentRoms = localStorage.getItem("settings.showRecentRoms");
const showRecentRomsRef = ref(
isNull(storedShowRecentRoms) ? true : storedShowRecentRoms === "true",
);
const storedGridRecentRoms = localStorage.getItem("settings.gridRecentRoms");
const gridRecentRomsRef = ref(
isNull(storedGridRecentRoms) ? true : storedGridRecentRoms === "true",
);
const storedShowPlatforms = localStorage.getItem("settings.showPlatforms");
const showPlatformsRef = ref(
isNull(storedShowPlatforms) ? true : storedShowPlatforms === "true",
);
const storedGridPlatforms = localStorage.getItem("settings.gridPlatforms");
const gridPlatformsRef = ref(
isNull(storedGridPlatforms) ? true : storedGridPlatforms === "true",
);
const storedShowCollections = localStorage.getItem("settings.showCollections");
const showCollectionsRef = ref(
isNull(storedShowCollections) ? true : storedShowCollections === "true",
);
const storedGridCollections = localStorage.getItem("settings.gridCollections");
const gridCollectionsRef = ref(
isNull(storedGridCollections) ? true : storedGridCollections === "true",
);
const storedGroupRoms = localStorage.getItem("settings.groupRoms");
const groupRomsRef = ref(
isNull(storedGroupRoms) ? true : storedGroupRoms === "true",
);
const storedSiblings = localStorage.getItem("settings.showSiblings");
const siblingsRef = ref(
isNull(storedSiblings) ? true : storedSiblings === "true",
);
const storedRegions = localStorage.getItem("settings.showRegions");
const regionsRef = ref(isNull(storedRegions) ? true : storedRegions === "true");
const storedLanguages = localStorage.getItem("settings.showLanguages");
const languagesRef = ref(
isNull(storedLanguages) ? true : storedLanguages === "true",
);
const storedStatus = localStorage.getItem("settings.showStatus");
const statusRef = ref(isNull(storedStatus) ? true : storedStatus === "true");
const homeOptions = computed(() => [
{
title: "Show recently added roms",
description: "Show recently added roms section at the home page",
iconEnabled: "mdi-checkbox-marked-outline",
iconDisabled: "mdi-checkbox-blank-outline",
model: showRecentRomsRef,
modelTrigger: toggleShowRecentRoms,
},
{
title: "Recently added roms as grid",
description: "View recently added rom cards as a grid at the home page",
iconEnabled: "mdi-view-comfy",
iconDisabled: "mdi-view-column",
disabled: !showRecentRomsRef.value,
model: gridRecentRomsRef,
modelTrigger: toggleGridRecentRoms,
},
{
title: "Show platforms",
description: "Show platform section at the home page",
iconEnabled: "mdi-checkbox-marked-outline",
iconDisabled: "mdi-checkbox-blank-outline",
model: showPlatformsRef,
modelTrigger: toggleShowPlatforms,
},
{
title: "Platforms as grid",
description: "View platform cards as a grid at the home page",
iconEnabled: "mdi-view-comfy",
iconDisabled: "mdi-view-column",
disabled: !showPlatformsRef.value,
model: gridPlatformsRef,
modelTrigger: toggleGridPlatforms,
},
{
title: "Show collections",
description: "Show collections section at the home page",
iconEnabled: "mdi-checkbox-marked-outline",
iconDisabled: "mdi-checkbox-blank-outline",
model: showCollectionsRef,
modelTrigger: toggleShowCollections,
},
{
title: "Collections a grid",
description: "View collection cards as a grid at the home page",
iconEnabled: "mdi-view-comfy",
iconDisabled: "mdi-view-column",
disabled: !showCollectionsRef.value,
model: gridCollectionsRef,
modelTrigger: toggleGridCollections,
},
]);
const galleryOptions = computed(() => [
{
title: "Group roms",
description: "Group versions of the same rom together in the gallery",
iconEnabled: "mdi-group",
iconDisabled: "mdi-ungroup",
model: groupRomsRef,
modelTrigger: toggleGroupRoms,
},
{
title: "Show siblings",
description:
'Show siblings count in the gallery when "Group roms" option is enabled',
iconEnabled: "mdi-account-group-outline",
iconDisabled: "mdi-account-outline",
model: siblingsRef,
disabled: !groupRomsRef.value,
modelTrigger: toggleSiblings,
},
{
title: "Show regions",
description: "Show region flags in the gallery",
iconEnabled: "mdi-flag-outline",
iconDisabled: "mdi-flag-off-outline",
model: regionsRef,
modelTrigger: toggleRegions,
},
{
title: "Show languages",
description: "Show language flags in the gallery",
iconEnabled: "mdi-flag-outline",
iconDisabled: "mdi-flag-off-outline",
model: languagesRef,
modelTrigger: toggleLanguages,
},
{
title: "Show status",
description:
"Show status icons in the gallery (backlogged, playing, completed, etc)",
iconEnabled: "mdi-check-circle-outline",
iconDisabled: "mdi-close-circle-outline",
model: statusRef,
modelTrigger: toggleStatus,
},
]);
// Functions to update localStorage
const toggleShowRecentRoms = (value: boolean) => {
showRecentRomsRef.value = value;
localStorage.setItem("settings.showRecentRoms", value.toString());
};
const toggleGridRecentRoms = (value: boolean) => {
gridRecentRomsRef.value = value;
localStorage.setItem("settings.gridRecentRoms", value.toString());
};
const toggleShowPlatforms = (value: boolean) => {
showPlatformsRef.value = value;
localStorage.setItem("settings.showPlatforms", value.toString());
};
const toggleGridPlatforms = (value: boolean) => {
gridPlatformsRef.value = value;
localStorage.setItem("settings.gridPlatforms", value.toString());
};
const toggleShowCollections = (value: boolean) => {
showCollectionsRef.value = value;
localStorage.setItem("settings.showCollections", value.toString());
};
const toggleGridCollections = (value: boolean) => {
gridCollectionsRef.value = value;
localStorage.setItem("settings.gridCollections", value.toString());
};
const toggleGroupRoms = (value: boolean) => {
groupRomsRef.value = value;
localStorage.setItem("settings.groupRoms", value.toString());
};
const toggleSiblings = (value: boolean) => {
siblingsRef.value = value;
localStorage.setItem("settings.showSiblings", value.toString());
};
const toggleRegions = (value: boolean) => {
regionsRef.value = value;
localStorage.setItem("settings.showRegions", value.toString());
};
const toggleLanguages = (value: boolean) => {
languagesRef.value = value;
localStorage.setItem("settings.showLanguages", value.toString());
};
const toggleStatus = (value: boolean) => {
statusRef.value = value;
localStorage.setItem("settings.showStatus", value.toString());
};
</script>
<template>
<r-section icon="mdi-palette-swatch-outline" title="Interface">
<template #content>
<v-chip label variant="text" prepend-icon="mdi-home" class="ml-2"
>Home</v-chip
>
<v-divider class="border-opacity-25 mx-2" />
<v-row class="py-1" no-gutters>
<v-col
cols="12"
md="6"
v-for="option in homeOptions"
:key="option.title"
>
<interface-option
class="mx-2"
:disabled="option.disabled"
:title="option.title"
:description="option.description"
:icon="
option.model.value ? option.iconEnabled : option.iconDisabled
"
v-model="option.model.value"
@update:model-value="option.modelTrigger"
/>
</v-col>
</v-row>
<v-chip
label
variant="text"
prepend-icon="mdi-view-grid"
class="ml-2 mt-4"
>Gallery</v-chip
>
<v-divider class="border-opacity-25 mx-2" />
<v-row class="py-1" no-gutters>
<v-col
cols="12"
md="6"
v-for="option in galleryOptions"
:key="option.title"
>
<interface-option
class="mx-2"
:disabled="option.disabled"
:title="option.title"
:description="option.description"
:icon="
option.model.value ? option.iconEnabled : option.iconDisabled
"
v-model="option.model.value"
@update:model-value="option.modelTrigger"
/>
</v-col>
</v-row>
</template>
</r-section>
</template>

View File

@@ -29,6 +29,7 @@ const emit = defineEmits(["update:modelValue"]);
<template #append>
<v-switch
inset
:model-value="modelValue"
@update:model-value="(value) => emit('update:modelValue', value)"
:class="{ 'pr-16': !xs }"

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
import ThemeOption from "@/components/Settings/ThemeOption.vue";
import ThemeOption from "@/components/Settings/UserInterface/ThemeOption.vue";
import RSection from "@/components/common/RSection.vue";
import { autoThemeKey, themes } from "@/styles/themes";
import { isKeyof } from "@/types";
import { computed, ref } from "vue";
import { useTheme } from "vuetify";
// Props
const theme = useTheme();
const storedTheme = parseInt(localStorage.getItem("settings.theme") ?? "");
const selectedTheme = ref(isNaN(storedTheme) ? autoThemeKey : storedTheme);
@@ -23,7 +24,6 @@ const themeOptions = computed(() => [
icon: "mdi-theme-light-dark",
},
]);
// Functions
function toggleTheme() {
localStorage.setItem("settings.theme", selectedTheme.value.toString());
@@ -35,7 +35,6 @@ function toggleTheme() {
}
}
</script>
<template>
<r-section icon="mdi-brush-variant" title="Theme">
<template #content>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import RAvatarCollection from "@/components/common/Collection/RAvatar.vue";
import RAvatarRom from "@/components/common/Game/RAvatar.vue";
import CollectionListItem from "@/components/common/Collection/ListItem.vue";
import RomListItem from "@/components/common/Game/ListItem.vue";
import RDialog from "@/components/common/RDialog.vue";
import type { UpdatedCollection } from "@/services/api/collection";
import collectionApi from "@/services/api/collection";
@@ -110,21 +111,21 @@ function closeDialog() {
clearable
>
<template #item="{ props, item }">
<v-list-item
class="py-4"
<collection-list-item
:collection="item.raw"
v-bind="props"
:title="item.raw.name ?? ''"
:subtitle="item.raw.description"
>
<template #prepend>
<r-avatar-collection :collection="item.raw" />
</template>
<template #append>
<v-chip class="ml-2" size="x-small" label>
{{ item.raw.rom_count }}
</v-chip>
</template>
</v-list-item>
:with-title="false"
/>
</template>
<template #chip="{ item }">
<v-chip class="pl-0" label>
<r-avatar-collection
:collection="item.raw"
:size="35"
class="mr-2"
/>
{{ item.raw.name }}
</v-chip>
</template>
</v-autocomplete>
</template>
@@ -140,14 +141,7 @@ function closeDialog() {
hide-default-header
>
<template #item.name="{ item }">
<v-list-item class="px-0">
<template #prepend>
<r-avatar-rom :rom="item" />
</template>
<v-row no-gutters
><v-col>{{ item.name }}</v-col></v-row
>
</v-list-item>
<rom-list-item :rom="item" with-filename />
</template>
<template #bottom>
<v-divider />

View File

@@ -158,10 +158,10 @@ function closeDialog() {
size="small"
class="translucent-dark"
@click="
emitter?.emit(
'showSearchCoverDialog',
collection.name as string,
)
emitter?.emit('showSearchCoverDialog', {
term: collection.name as string,
aspectRatio: null,
})
"
>
<v-icon size="large">mdi-image-search-outline</v-icon>

View File

@@ -47,7 +47,7 @@ async function deleteCollection() {
return;
});
await router.push({ name: "dashboard" });
await router.push({ name: "home" });
collectionsStore.remove(collection.value);
emitter?.emit("refreshDrawer", null);
@@ -71,7 +71,7 @@ function closeDialog() {
<v-row class="justify-center align-center pa-2" no-gutters>
<span>Removing collection</span>
<r-avatar class="ml-1" :collection="collection" />
<span class="ml-1 text-romm-accent-1">{{ collection.name }}</span>
<span class="ml-1 text-romm-accent-1">{{ collection.name }}.</span>
<span class="ml-1">from RomM. Do you confirm?</span>
</v-row>
</template>

View File

@@ -12,7 +12,7 @@ import { useDisplay, useTheme } from "vuetify";
// Props
const theme = useTheme();
const { mdAndUp } = useDisplay();
const { smAndDown, mdAndUp, lgAndUp } = useDisplay();
const show = ref(false);
const storeCollection = collectionStore();
const collection = ref<UpdatedCollection>({} as UpdatedCollection);
@@ -106,12 +106,12 @@ function closeDialog() {
@close="closeDialog"
v-model="show"
icon="mdi-pencil-box"
:width="mdAndUp ? '55vw' : '95vw'"
:width="lgAndUp ? '65vw' : '95vw'"
>
<template #content>
<v-row class="align-center pa-2" no-gutters>
<v-col cols="12" lg="7" xl="9">
<v-row class="pa-2" no-gutters>
<v-col cols="12" md="8" xl="9">
<v-row class="px-2" no-gutters>
<v-col>
<v-text-field
v-model="collection.name"
@@ -124,7 +124,7 @@ function closeDialog() {
/>
</v-col>
</v-row>
<v-row class="pa-2" no-gutters>
<v-row class="px-2" no-gutters>
<v-col>
<v-text-field
v-model="collection.description"
@@ -137,25 +137,33 @@ function closeDialog() {
/>
</v-col>
</v-row>
<v-row class="pa-2" no-gutters>
<v-row class="px-2" no-gutters>
<v-col>
<v-switch
v-model="collection.is_public"
color="romm-accent-1"
class="px-2"
false-icon="mdi-lock"
true-icon="mdi-lock-open"
inset
hide-details
message="Public (visible to everyone)"
:label="
collection.is_public
? 'Public (visible to everyone)'
: 'Private (only visible to me)'
"
color="romm-accent-1"
class="px-2"
hide-details
/>
</v-col>
</v-row>
</v-col>
<v-col>
<v-row class="pa-2 justify-center" no-gutters>
<v-col class="cover">
<v-col cols="12" md="4" xl="3">
<v-row
class="justify-center"
:class="{ 'mt-4': smAndDown }"
no-gutters
>
<v-col style="max-width: 240px">
<collection-card
:key="collection.updated_at"
:show-title="false"
@@ -172,10 +180,10 @@ function closeDialog() {
size="small"
class="translucent-dark"
@click="
emitter?.emit(
'showSearchCoverDialog',
collection.name as string,
)
emitter?.emit('showSearchCoverDialog', {
term: collection.name as string,
aspectRatio: null,
})
"
>
<v-icon size="large">mdi-image-search-outline</v-icon>
@@ -211,9 +219,7 @@ function closeDialog() {
</v-row>
</v-col>
</v-row>
</template>
<template #append>
<v-row class="justify-center mt-4 mb-2" no-gutters>
<v-row class="justify-center pa-2 mt-1" no-gutters>
<v-btn-group divided density="compact">
<v-btn class="bg-terciary" @click="closeDialog"> Cancel </v-btn>
<v-btn class="text-romm-green bg-terciary" @click="editCollection">
@@ -224,11 +230,3 @@ function closeDialog() {
</template>
</r-dialog>
</template>
<style scoped>
.cover {
min-width: 240px;
min-height: 330px;
max-width: 240px;
max-height: 330px;
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import RAvatarRom from "@/components/common/Game/RAvatar.vue";
import RomListItem from "@/components/common/Game/ListItem.vue";
import RDialog from "@/components/common/RDialog.vue";
import router from "@/plugins/router";
import collectionApi from "@/services/api/collection";
@@ -66,7 +66,7 @@ async function removeRomsFromCollection() {
emitter?.emit("showLoadingDialog", { loading: false, scrim: false });
romsStore.resetSelection();
if (selectedCollection.value?.roms.length == 0) {
router.push({ name: "dashboard" });
router.push({ name: "home" });
}
closeDialog();
});
@@ -118,14 +118,7 @@ function closeDialog() {
hide-default-header
>
<template #item.name="{ item }">
<v-list-item class="px-0">
<template #prepend>
<r-avatar-rom :rom="item" />
</template>
<v-row no-gutters
><v-col>{{ item.name }}</v-col></v-row
>
</v-list-item>
<rom-list-item :rom="item" with-filename />
</template>
<template #bottom>
<v-divider />

View File

@@ -3,29 +3,50 @@ import type { Collection } from "@/stores/collections";
import RAvatar from "@/components/common/Collection/RAvatar.vue";
// Props
defineProps<{ collection: Collection }>();
withDefaults(
defineProps<{
collection: Collection;
withTitle?: boolean;
withDescription?: boolean;
withRomCount?: boolean;
withLink?: boolean;
}>(),
{
withTitle: true,
withDescription: true,
withRomCount: true,
withLink: false,
},
);
</script>
<template>
<v-list-item
:key="collection.id"
:to="{ name: 'collection', params: { collection: collection.id } }"
v-bind="{
...(withLink && collection
? {
to: { name: 'collection', params: { collection: collection.id } },
}
: {}),
}"
:value="collection.name"
class="py-1 pl-1"
>
<template #prepend>
<r-avatar :collection="collection" />
<r-avatar :size="75" :collection="collection" />
</template>
<v-row no-gutters
<v-row v-if="withTitle" no-gutters
><v-col
><span class="text-body-1">{{ collection.name }}</span></v-col
></v-row
>
<v-row no-gutters>
<v-row v-if="withDescription" no-gutters>
<v-col>
<span class="text-caption text-grey">{{ collection.description }}</span>
</v-col>
</v-row>
<template #append>
<template v-if="withRomCount" #append>
<v-chip class="ml-2" size="x-small" label>
{{ collection.rom_count }}
</v-chip>

View File

@@ -18,6 +18,8 @@ const props = withDefaults(
defineProps<{
rom: SimpleRom | SearchRomSchema;
aspectRatio?: string | number;
width?: string | number;
height?: string | number;
transformScale?: boolean;
titleOnHover?: boolean;
showFlags?: boolean;
@@ -28,11 +30,13 @@ const props = withDefaults(
showFav?: boolean;
withBorder?: boolean;
withBorderRommAccent?: boolean;
withLink: boolean;
withLink?: boolean;
src?: string;
}>(),
{
aspectRatio: undefined,
width: undefined,
height: undefined,
transformScale: false,
titleOnHover: false,
showFlags: false,
@@ -63,7 +67,6 @@ const handleTouchEnd = (event: TouchEvent) => {
emit("touchend", { event: event, rom: props.rom });
};
const downloadStore = storeDownload();
const card = ref();
const theme = useTheme();
const galleryViewStore = storeGalleryView();
const collectionsStore = storeCollections();
@@ -79,6 +82,10 @@ const computedAspectRatio = computed(() => {
<template>
<v-hover v-slot="{ isHovering, props: hoverProps }">
<v-card
:minWidth="width"
:maxWidth="width"
:minHeight="height"
:maxHeight="height"
v-bind="{
...hoverProps,
...(withLink && rom && romsStore.isSimpleRom(rom)
@@ -110,23 +117,19 @@ const computedAspectRatio = computed(() => {
@touchend="handleTouchEnd"
v-bind="hoverProps"
:class="{ pointer: pointerOnHover }"
ref="card"
cover
:key="romsStore.isSimpleRom(rom) ? rom.updated_at : ''"
:src="
src
? src
: romsStore.isSimpleRom(rom)
? !rom.igdb_id && !rom.moby_id && !rom.has_cover
? `/assets/default/cover/big_${theme.global.name.value}_unmatched.png`
: (rom.igdb_id || rom.moby_id) && !rom.has_cover
? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
: `/assets/romm/resources/${rom.path_cover_l}?ts=${rom.updated_at}`
: !rom.igdb_url_cover && !rom.moby_url_cover
src ||
(romsStore.isSimpleRom(rom)
? !rom.igdb_id && !rom.moby_id && !rom.has_cover
? `/assets/default/cover/big_${theme.global.name.value}_unmatched.png`
: (rom.igdb_id || rom.moby_id) && !rom.has_cover
? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
: rom.igdb_url_cover
? rom.igdb_url_cover
: rom.moby_url_cover
: `/assets/romm/resources/${rom.path_cover_l}?ts=${rom.updated_at}`
: !rom.igdb_url_cover && !rom.moby_url_cover
? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
: rom.igdb_url_cover || rom.moby_url_cover)
"
:lazy-src="
romsStore.isSimpleRom(rom)
@@ -137,9 +140,7 @@ const computedAspectRatio = computed(() => {
: `/assets/romm/resources/${rom.path_cover_s}?ts=${rom.updated_at}`
: !rom.igdb_url_cover && !rom.moby_url_cover
? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
: rom.igdb_url_cover
? rom.igdb_url_cover
: rom.moby_url_cover
: rom.igdb_url_cover || rom.moby_url_cover
"
:aspect-ratio="computedAspectRatio"
>
@@ -155,10 +156,6 @@ const computedAspectRatio = computed(() => {
!rom.moby_url_cover)
"
class="translucent-dark text-caption text-white"
:class="{
'text-truncate':
galleryViewStore.currentView == 0 && !isHovering,
}"
>
<v-list-item>{{ rom.name }}</v-list-item>
</div>
@@ -209,7 +206,7 @@ const computedAspectRatio = computed(() => {
class="position-absolute append-inner-left"
v-if="!showPlatformIcon"
>
<slot name="append-inner-left"> </slot>
<slot name="append-inner-left"></slot>
</div>
<div class="position-absolute append-inner-right" v-if="!showFav">
<slot name="append-inner-right"> </slot>

View File

@@ -9,49 +9,41 @@ const props = defineProps<{
}>();
const theme = useTheme();
const galleryViewStore = storeGalleryView();
const handleClick = () => {
if (props.game.slug) {
window.open(
`https://www.igdb.com/games/${props.game.slug}`,
"_blank",
"noopener noreferrer",
);
}
};
</script>
<template>
<v-card class="ma-1" v-on:click="handleClick">
<v-tooltip
activator="parent"
location="top"
class="tooltip"
transition="fade-transition"
open-delay="1000"
>{{ game.name }}</v-tooltip
>
<!-- TODO: fix aspect ratio -->
<v-img
v-bind="props"
:src="
`${game.cover_url}`
? `https:${game.cover_url.replace('t_thumb', 't_cover_big')}`
: `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
"
:aspect-ratio="galleryViewStore.defaultAspectRatioCover"
cover
lazy
><v-chip
class="px-2 position-absolute chip-type text-white translucent-dark"
density="compact"
label
<a :href="`https://www.igdb.com/games/${game.slug}`" target="_blank">
<v-card>
<v-tooltip
activator="parent"
location="top"
class="tooltip"
transition="fade-transition"
open-delay="1000"
>{{ game.name }}</v-tooltip
>
<span>
{{ game.type }}
</span>
</v-chip></v-img
>
</v-card>
<v-img
v-bind="props"
:src="
game.cover_url ||
`/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
"
:aspect-ratio="galleryViewStore.defaultAspectRatioCover"
cover
lazy
><v-chip
class="px-2 position-absolute chip-type text-white translucent-dark"
density="compact"
rounded="0"
label
>
<span>
{{ game.type }}
</span>
</v-chip></v-img
>
</v-card>
</a>
</template>
<style scoped>
.chip-type {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import RAvatar from "@/components/common/Game/RAvatar.vue";
import RomListItem from "@/components/common/Game/ListItem.vue";
import RDialog from "@/components/common/RDialog.vue";
import romApi from "@/services/api/rom";
import storeRoms, { type SimpleRom } from "@/stores/roms";
@@ -122,53 +122,17 @@ function closeDialog() {
show-select
>
<template #item.name="{ item }">
<v-list-item class="px-0">
<template #prepend>
<r-avatar :rom="item" />
</template>
<v-row no-gutters
><v-col>{{ item.name }}</v-col></v-row
>
<v-row v-if="romsToDeleteFromFs.includes(item.id)" no-gutters
><v-col class="text-romm-accent-1"
>{{ item.file_name
}}<v-chip
v-if="romsToDeleteFromFs.includes(item.id) && smAndUp"
label
size="x-small"
class="text-romm-red ml-1"
>
Removing from filesystem
</v-chip></v-col
></v-row
>
<v-row
v-if="romsToDeleteFromFs.includes(item.id) && !smAndUp"
no-gutters
>
<v-col>
<v-chip label size="x-small" class="text-romm-red">
Removing from filesystem
</v-chip>
</v-col>
</v-row>
<v-row no-gutters>
<v-col>
<v-chip v-if="!smAndUp" size="x-small" label
>{{ formatBytes(item.file_size_bytes) }}
</v-chip>
</v-col>
</v-row>
<template #append>
<v-row no-gutters v-if="smAndUp">
<rom-list-item :rom="item" with-filename>
<template #append-body>
<v-row v-if="romsToDeleteFromFs.includes(item.id)" no-gutters>
<v-col>
<v-chip size="x-small" label>{{
formatBytes(item.file_size_bytes)
}}</v-chip>
<v-chip label size="x-small" class="text-romm-red">
Removing from filesystem
</v-chip>
</v-col>
</v-row>
</template>
</v-list-item>
</rom-list-item>
</template>
<template #bottom>
<v-divider />

View File

@@ -2,7 +2,9 @@
import GameCard from "@/components/common/Game/Card/Base.vue";
import RDialog from "@/components/common/RDialog.vue";
import romApi, { type UpdateRom } from "@/services/api/rom";
import storeGalleryView from "@/stores/galleryView";
import storeHeartbeat from "@/stores/heartbeat";
import storePlatforms from "@/stores/platforms";
import storeRoms, { type SimpleRom } from "@/stores/roms";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
@@ -20,6 +22,8 @@ const rom = ref<UpdateRom>();
const romsStore = storeRoms();
const imagePreviewUrl = ref<string | undefined>("");
const removeCover = ref(false);
const platfotmsStore = storePlatforms();
const galleryViewStore = storeGalleryView();
const emitter = inject<Emitter<Events>>("emitter");
emitter?.on("showEditRomDialog", (romToEdit: UpdateRom | undefined) => {
show.value = true;
@@ -31,6 +35,12 @@ emitter?.on("updateUrlCover", (url_cover) => {
rom.value.url_cover = url_cover;
setArtwork(url_cover);
});
const computedAspectRatio = computed(() => {
const ratio = rom.value?.platform_id
? platfotmsStore.getAspectRatio(rom.value?.platform_id)
: galleryViewStore.defaultAspectRatioCover;
return parseFloat(ratio.toString());
});
// Functions
function triggerFileInput() {
@@ -198,34 +208,14 @@ function closeDialog() {
/>
</v-col>
</v-row>
<v-row
v-if="mdAndUp"
class="justify-space-between mt-4 mb-2 mx-2"
no-gutters
>
<v-btn-group divided density="compact">
<v-btn
:disabled="noMetadataMatch"
:class="` ${
noMetadataMatch ? '' : 'bg-terciary text-romm-red'
}`"
variant="flat"
@click="unmatchRom"
>
Unmatch Rom
</v-btn>
</v-btn-group>
<v-btn-group divided density="compact">
<v-btn class="bg-terciary" @click="closeDialog"> Cancel </v-btn>
<v-btn class="text-romm-green bg-terciary" @click="updateRom">
Save
</v-btn>
</v-btn-group>
</v-row>
</v-col>
<v-col cols="12" md="4" xl="3">
<v-row class="justify-center" no-gutters>
<v-col style="max-width: 240px" :class="{ 'my-4': smAndDown }">
<v-row
class="justify-center"
:class="{ 'mt-4': smAndDown }"
no-gutters
>
<v-col style="max-width: 240px">
<game-card :rom="rom" :src="imagePreviewUrl">
<template #append-inner-right>
<v-btn-group rounded="0" divided density="compact">
@@ -236,10 +226,10 @@ function closeDialog() {
size="small"
class="translucent-dark"
@click="
emitter?.emit(
'showSearchCoverDialog',
rom.name as string,
)
emitter?.emit('showSearchCoverDialog', {
term: rom.name as string,
aspectRatio: computedAspectRatio,
})
"
>
<v-icon size="large">mdi-image-search-outline</v-icon>
@@ -273,28 +263,24 @@ function closeDialog() {
</game-card>
</v-col>
</v-row>
<v-row v-if="smAndDown" class="justify-space-between pa-2" no-gutters>
<v-btn-group divided density="compact" class="my-1">
<v-btn
:disabled="noMetadataMatch"
:class="` ${
noMetadataMatch ? '' : 'bg-terciary text-romm-red'
}`"
variant="flat"
@click="unmatchRom"
>
Unmatch Rom
</v-btn>
</v-btn-group>
<v-btn-group divided density="compact" class="my-1">
<v-btn class="bg-terciary" @click="closeDialog"> Cancel </v-btn>
<v-btn class="text-romm-green bg-terciary" @click="updateRom">
Save
</v-btn>
</v-btn-group>
</v-row>
</v-col>
</v-row>
<v-row class="justify-space-between px-4 py-2 mt-1" no-gutters>
<v-btn
:disabled="noMetadataMatch"
:class="` ${noMetadataMatch ? '' : 'bg-terciary text-romm-red'}`"
variant="flat"
@click="unmatchRom"
>
Unmatch Rom
</v-btn>
<v-btn-group divided density="compact">
<v-btn class="bg-terciary" @click="closeDialog"> Cancel </v-btn>
<v-btn class="text-romm-green bg-terciary" @click="updateRom">
Save
</v-btn>
</v-btn-group>
</v-row>
</template>
</r-dialog>
</template>

View File

@@ -244,10 +244,13 @@ onBeforeUnmount(() => {
<v-avatar
@click="toggleSourceFilter('IGDB')"
v-bind="props"
class="ml-3 source-filter"
class="ml-3 cursor-pointer opacity-40"
:class="{
filtered: isIGDBFiltered,
disabled: !heartbeat.value.METADATA_SOURCES.IGDB_API_ENABLED,
'opacity-100':
isIGDBFiltered &&
heartbeat.value.METADATA_SOURCES.IGDB_API_ENABLED,
'cursor-not-allowed':
!heartbeat.value.METADATA_SOURCES.IGDB_API_ENABLED,
}"
size="30"
rounded="1"
@@ -270,10 +273,13 @@ onBeforeUnmount(() => {
<v-avatar
@click="toggleSourceFilter('Mobygames')"
v-bind="props"
class="ml-3 source-filter"
class="ml-3 cursor-pointer opacity-40"
:class="{
filtered: isMobyFiltered,
disabled: !heartbeat.value.METADATA_SOURCES.MOBY_API_ENABLED,
'opacity-100':
isMobyFiltered &&
heartbeat.value.METADATA_SOURCES.MOBY_API_ENABLED,
'cursor-not-allowed':
!heartbeat.value.METADATA_SOURCES.MOBY_API_ENABLED,
}"
size="30"
rounded="1"
@@ -322,7 +328,7 @@ onBeforeUnmount(() => {
</v-row>
</template>
<template #content>
<v-row v-show="!showSelectSource" no-gutters>
<v-row class="align-content-start" v-show="!showSelectSource" no-gutters>
<v-col
class="pa-1"
cols="4"
@@ -335,9 +341,8 @@ onBeforeUnmount(() => {
v-if="rom"
@click="showSources(matchedRom)"
:rom="matchedRom"
title-on-footer
transform-scale
title-on-hover
transformScale
titleOnHover
pointerOnHover
/>
</v-col>
@@ -372,16 +377,10 @@ onBeforeUnmount(() => {
</v-col>
<v-col cols="12">
<v-row class="justify-center mt-4" no-gutters>
<v-col
:class="{
'source-cover-desktop': !xs,
'source-cover-mobile': xs,
}"
class="pa-1"
v-for="source in sources"
>
<v-col class="pa-1" cols="auto" v-for="source in sources">
<v-hover v-slot="{ isHovering, props }">
<v-card
:width="xs ? 150 : 220"
v-bind="props"
class="transform-scale mx-2"
:class="{
@@ -495,28 +494,3 @@ onBeforeUnmount(() => {
</template>
</r-dialog>
</template>
<style scoped>
.source-filter {
cursor: pointer;
opacity: 0.4;
}
.source-filter.filtered {
opacity: 1;
}
.source-filter.disabled {
cursor: not-allowed !important;
opacity: 0.4 !important;
}
.select-source-dialog {
z-index: 9999 !important;
}
.source-cover-desktop {
min-width: 220px;
max-width: 220px;
}
.source-cover-mobile {
min-width: 150px;
max-width: 150px;
}
</style>

View File

@@ -200,11 +200,11 @@ onBeforeUnmount(() => {
title-on-hover
pointerOnHover
withLink
show-flags
show-fav
transform-scale
show-action-bar
show-platform-icon
showFlags
showFav
transformScale
showActionBar
showPlatformIcon
/>
</v-col>
</v-row>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import type { SimpleRom } from "@/stores/roms";
import RAvatarRom from "@/components/common/Game/RAvatar.vue";
import { formatBytes } from "@/utils";
import { useDisplay } from "vuetify";
// Props
withDefaults(
defineProps<{
rom: SimpleRom;
withAvatar?: boolean;
withName?: boolean;
withFilename?: boolean;
withSize?: boolean;
withLink?: boolean;
}>(),
{
withAvatar: true,
withName: true,
withFilename: false,
withSize: true,
withLink: false,
},
);
</script>
<template>
<v-list-item
v-bind="{
...(withLink && rom
? {
to: { name: 'rom', params: { rom: rom.id } },
}
: {}),
}"
>
<template v-if="withAvatar" #prepend>
<slot name="prepend"></slot>
<r-avatar-rom :rom="rom" />
</template>
<v-row v-if="withName" no-gutters
><v-col>{{ rom.name }}</v-col></v-row
>
<v-row v-if="withFilename" no-gutters
><v-col class="text-romm-accent-1">{{ rom.file_name }}</v-col></v-row
>
<slot name="append-body"></slot>
<template #append>
<v-row no-gutters>
<v-col v-if="withSize" cols="auto">
<v-chip size="x-small" label>{{
formatBytes(rom.file_size_bytes)
}}</v-chip>
</v-col>
<v-col>
<slot name="append"></slot>
</v-col>
</v-row>
</template>
</v-list-item>
</template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import AdminMenu from "@/components/common/Game/AdminMenu.vue";
import FavBtn from "@/components/common/Game/FavBtn.vue";
import RAvatar from "@/components/common/Game/RAvatar.vue";
import RAvatarRom from "@/components/common/Game/RAvatar.vue";
import romApi from "@/services/api/rom";
import storeDownload from "@/stores/download";
import storeRoms, { type SimpleRom } from "@/stores/roms";
@@ -154,10 +154,10 @@ onMounted(() => {
<td class="name-row">
<v-list-item class="px-0">
<template #prepend>
<r-avatar :rom="item" />
<r-avatar-rom :rom="item" />
</template>
<v-row no-gutters
><v-col>{{ item.name }}</v-col></v-row
<v-row no-gutters>
<v-col>{{ item.name }}</v-col></v-row
>
<v-row no-gutters
><v-col class="text-romm-accent-1">{{

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import CollectionListItem from "@/components/common/Collection/ListItem.vue";
import storeCollections from "@/stores/collections";
import CreateCollectionDialog from "@/components/common/Collection/Dialog/CreateCollection.vue";
import storeNavigation from "@/stores/navigation";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
@@ -50,6 +51,7 @@ function clear() {
<collection-list-item
v-for="collection in filteredCollections"
:collection="collection"
with-link
/>
</v-list>
<template #append>
@@ -65,4 +67,6 @@ function clear() {
>
</template>
</v-navigation-drawer>
<create-collection-dialog />
</template>

View File

@@ -1,25 +1,26 @@
<script setup lang="ts">
import { ref } from "vue";
import RommIso from "@/components/common/RommIso.vue";
import RIsotipo from "@/components/common/RIsotipo.vue";
import storeNavigation from "@/stores/navigation";
const homeUrl = ref(`${location.protocol}//${location.host}`);
const navigationStore = storeNavigation();
</script>
<template>
<a
@click.prevent
style="text-decoration: none; color: inherit"
:href="homeUrl"
>
<v-hover v-slot="{ isHovering, props: hoverProps }">
<romm-iso
@click="navigationStore.goHome"
v-bind="hoverProps"
class="pointer"
:class="{ 'border-romm-accent-1': isHovering }"
:size="35"
/>
</v-hover>
</a>
<r-isotipo
:to="homeUrl"
@click="navigationStore.goHome"
class="cursor-pointer"
:size="40"
/>
</template>
<style scoped>
.v-avatar {
transition: filter 0.15s ease-in-out;
}
.v-avatar:hover,
.v-avatar.active {
filter: drop-shadow(0px 0px 2px rgba(var(--v-theme-romm-accent-1)));
}
</style>

View File

@@ -6,9 +6,25 @@ import ScanBtn from "@/components/common/Navigation/ScanBtn.vue";
import SearchBtn from "@/components/common/Navigation/SearchBtn.vue";
import UploadBtn from "@/components/common/Navigation/UploadBtn.vue";
import UserBtn from "@/components/common/Navigation/UserBtn.vue";
import SearchRomDialog from "@/components/common/Game/Dialog/SearchRom.vue";
import PlatformsDrawer from "@/components/common/Navigation/PlatformsDrawer.vue";
import CollectionsDrawer from "@/components/common/Navigation/CollectionsDrawer.vue";
import UploadRomDialog from "@/components/common/Game/Dialog/UploadRom.vue";
import SettingsDrawer from "@/components/common/Navigation/SettingsDrawer.vue";
import storeNavigation from "@/stores/navigation";
import { storeToRefs } from "pinia";
import { useDisplay } from "vuetify";
// Props
const { smAndDown } = useDisplay();
const navigationStore = storeNavigation();
const { activePlatformsDrawer, activeCollectionsDrawer, activeSettingsDrawer } =
storeToRefs(navigationStore);
</script>
<template>
<!-- Mobile app bar -->
<v-app-bar
v-if="smAndDown"
elevation="0"
class="bg-primary justify-center px-1"
mode="shift"
@@ -32,4 +48,40 @@ import UserBtn from "@/components/common/Navigation/UserBtn.vue";
<user-btn />
</template>
</v-app-bar>
<!-- Desktop app bar -->
<v-navigation-drawer
v-else
permanent
rail
:floating="
activePlatformsDrawer || activeCollectionsDrawer || activeSettingsDrawer
"
rail-width="60"
>
<template #prepend>
<v-row no-gutters class="my-2 justify-center">
<home-btn />
</v-row>
</template>
<v-divider opacity="0" class="my-4" />
<search-btn block />
<platforms-btn block />
<collections-btn block />
<v-divider opacity="0" class="my-3" />
<upload-btn block />
<scan-btn block />
<template #append>
<v-row no-gutters class="my-2 justify-center">
<user-btn />
</v-row>
</template>
</v-navigation-drawer>
<search-rom-dialog />
<platforms-drawer />
<collections-drawer />
<upload-rom-dialog />
<settings-drawer />
</template>

View File

@@ -1,45 +0,0 @@
<script setup lang="ts">
import HomeBtn from "@/components/common/Navigation/HomeBtn.vue";
import PlatformsBtn from "@/components/common/Navigation/PlatformsBtn.vue";
import CollectionsBtn from "@/components/common/Navigation/CollectionsBtn.vue";
import ScanBtn from "@/components/common/Navigation/ScanBtn.vue";
import SearchBtn from "@/components/common/Navigation/SearchBtn.vue";
import UploadBtn from "@/components/common/Navigation/UploadBtn.vue";
import UserBtn from "@/components/common/Navigation/UserBtn.vue";
import storeNavigation from "@/stores/navigation";
import { storeToRefs } from "pinia";
// Props
const navigationStore = storeNavigation();
const { activePlatformsDrawer, activeCollectionsDrawer, activeSettingsDrawer } =
storeToRefs(navigationStore);
</script>
<template>
<v-navigation-drawer
permanent
rail
:floating="
activePlatformsDrawer || activeCollectionsDrawer || activeSettingsDrawer
"
rail-width="60"
>
<template #prepend>
<v-row no-gutters class="my-2 justify-center">
<home-btn />
</v-row>
</template>
<v-divider opacity="0" class="my-4" />
<search-btn block />
<platforms-btn block />
<collections-btn block />
<v-divider opacity="0" class="my-3" />
<upload-btn block />
<scan-btn block />
<template #append>
<v-row no-gutters class="my-2 justify-center">
<user-btn />
</v-row>
</template>
</v-navigation-drawer>
</template>

View File

@@ -72,13 +72,13 @@ async function logout() {
append-icon="mdi-account"
>Profile</v-list-item
>
<v-list-item :to="{ name: 'settings' }" append-icon="mdi-palette"
>UI Settings</v-list-item
<v-list-item :to="{ name: 'userInterface' }" append-icon="mdi-palette"
>User Interface</v-list-item
>
<v-list-item
v-if="scopes.includes('platforms.write')"
append-icon="mdi-table-cog"
:to="{ name: 'management' }"
:to="{ name: 'libraryManagement' }"
>Library Management
</v-list-item>
<v-list-item

View File

@@ -13,7 +13,7 @@ const { user } = storeToRefs(auth);
<v-avatar
@click="navigationStore.switchActiveSettingsDrawer"
class="pointer"
size="35"
:size="40"
:class="{ active: navigationStore.activeSettingsDrawer }"
>
<v-img

View File

@@ -5,11 +5,13 @@ import { storeToRefs } from "pinia";
import { ref, watch } from "vue";
import { useDisplay } from "vuetify";
// Props
const { xs } = useDisplay();
const uploadStore = storeUpload();
const { files } = storeToRefs(uploadStore);
const show = ref(false);
// Functions
function clearFinished() {
uploadStore.clearFinished();
}
@@ -24,7 +26,7 @@ watch(files, (newList) => {
id="upload-in-progress"
v-model="show"
transition="scroll-y-transition"
:timeout="100000000000"
:timeout="-1"
absolute
:location="xs ? 'bottom' : 'bottom right'"
class="mb-4 mr-4"

View File

@@ -43,7 +43,7 @@ async function deletePlatform() {
return;
});
await router.push({ name: "dashboard" });
await router.push({ name: "home" });
platformsStore.remove(platform.value);
emitter?.emit("refreshDrawer", null);

View File

@@ -13,12 +13,14 @@ withDefaults(defineProps<{ platform: Platform; rail?: boolean }>(), {
:key="platform.slug"
:to="{ name: 'platform', params: { platform: platform.id } }"
:value="platform.slug"
class="py-0"
>
<template #prepend>
<platform-icon
:key="platform.slug"
:slug="platform.slug"
:name="platform.name"
:size="50"
>
<v-tooltip
location="bottom"

View File

@@ -2,7 +2,7 @@
import EmptyFirmware from "@/components/common/EmptyFirmware.vue";
import EmptyGame from "@/components/common/EmptyGame.vue";
import EmptyPlatform from "@/components/common/EmptyPlatform.vue";
import RommIso from "@/components/common/RommIso.vue";
import RIsotipo from "@/components/common/RIsotipo.vue";
import { onMounted, ref, useSlots } from "vue";
// Props
@@ -65,7 +65,7 @@ onMounted(() => {
<v-card rounded="0" :min-height="height" :max-height="height">
<v-toolbar density="compact" class="bg-terciary">
<v-icon v-if="icon" :icon="icon" class="ml-5" />
<romm-iso :size="30" class="mx-4" v-if="showRommIcon" />
<r-isotipo :size="30" class="mx-4" v-if="showRommIcon" />
<slot name="header"></slot>
<template #append>
<v-btn

View File

@@ -2,11 +2,12 @@
import type { SearchCoverSchema } from "@/__generated__";
import RDialog from "@/components/common/RDialog.vue";
import sgdbApi from "@/services/api/sgdb";
import storeGalleryView from "@/stores/galleryView";
import storePlatforms from "@/stores/platforms";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, onBeforeUnmount, ref } from "vue";
import { useDisplay } from "vuetify";
import storeGalleryView from "@/stores/galleryView";
// Props
const { lgAndUp } = useDisplay();
@@ -19,9 +20,14 @@ const filteredCovers = ref<SearchCoverSchema[]>();
const galleryViewStore = storeGalleryView();
const panels = ref([0]);
const emitter = inject<Emitter<Events>>("emitter");
emitter?.on("showSearchCoverDialog", (term) => {
const coverAspectRatio = ref(
parseFloat(galleryViewStore.defaultAspectRatioCover.toString()),
);
emitter?.on("showSearchCoverDialog", ({ term, aspectRatio = null }) => {
searchTerm.value = term;
show.value = true;
// TODO: set default aspect ratio to 2/3
if (aspectRatio) coverAspectRatio.value = aspectRatio;
if (searchTerm.value) searchCovers();
});
@@ -168,7 +174,7 @@ onBeforeUnmount(() => {
<v-list-item class="pa-0">{{ game.name }}</v-list-item>
</v-row>
</v-expansion-panel-title>
<v-expansion-panel-text class="pa-0">
<v-expansion-panel-text class="py-1">
<v-row no-gutters>
<v-col
class="pa-1"
@@ -184,7 +190,7 @@ onBeforeUnmount(() => {
:class="{ 'on-hover': isHovering }"
class="transform-scale pointer"
@click="selectCover(resource.url)"
:aspect-ratio="galleryViewStore.defaultAspectRatioCover"
:aspect-ratio="coverAspectRatio"
:src="resource.thumb"
cover
>
@@ -218,3 +224,8 @@ onBeforeUnmount(() => {
</template>
</r-dialog>
</template>
<style lang="css">
.v-expansion-panel-text__wrapper {
padding: 0px !important;
}
</style>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { ref, inject } from "vue";
import type { Emitter } from "mitt";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, ref } from "vue";
// Props
const show = ref(false);
const scrim = ref(false);
const emitter = inject<Emitter<Events>>("emitter");
emitter?.on("showLoadingDialog", (args) => {
show.value = args.loading;

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import Notification from "@/components/common/Notifications/Notification.vue";
import storeHeartbeat from "@/stores/heartbeat";
// Props
const heartbeatStore = storeHeartbeat();
</script>
<template>
<span class="h-100 w-100 position-absolute" id="bg" />
<notification />
<v-container class="fill-height justify-center">
<router-view />
</v-container>
<div id="version" class="position-absolute">
<span class="text-white text-shadow">{{
heartbeatStore.value.VERSION
}}</span>
</div>
</template>
<style scoped>
#bg {
background: url("/assets/login_bg.png") center center;
background-size: cover;
}
#version {
text-shadow:
1px 1px 1px #000000,
0 0 1px #000000;
bottom: 0.3rem;
right: 0.5rem;
}
</style>

View File

@@ -0,0 +1,107 @@
<script setup lang="ts">
import EditUserDialog from "@/components/Settings/Administration/Users/Dialog/EditUser.vue";
import AddRomsToCollectionDialog from "@/components/common/Collection/Dialog/AddRoms.vue";
import RemoveRomsFromCollectionDialog from "@/components/common/Collection/Dialog/RemoveRoms.vue";
import DeleteRomDialog from "@/components/common/Game/Dialog/DeleteRom.vue";
import EditRomDialog from "@/components/common/Game/Dialog/EditRom.vue";
import MatchRomDialog from "@/components/common/Game/Dialog/MatchRom.vue";
import MainAppBar from "@/components/common/Navigation/MainAppBar.vue";
import NewVersionDialog from "@/components/common/NewVersionDialog.vue";
import Notification from "@/components/common/Notifications/Notification.vue";
import UploadProgress from "@/components/common/Notifications/UploadProgress.vue";
import SearchCoverDialog from "@/components/common/SearchCover.vue";
import ViewLoader from "@/components/common/ViewLoader.vue";
import router from "@/plugins/router";
import collectionApi from "@/services/api/collection";
import api from "@/services/api/index";
import platformApi from "@/services/api/platform";
import userApi from "@/services/api/user";
import storeAuth from "@/stores/auth";
import storeCollections from "@/stores/collections";
import storeConfig from "@/stores/config";
import storeHeartbeat from "@/stores/heartbeat";
import storeNavigation from "@/stores/navigation";
import storePlatforms from "@/stores/platforms";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, onBeforeMount } from "vue";
// Props
const heartbeat = storeHeartbeat();
const configStore = storeConfig();
const navigationStore = storeNavigation();
const auth = storeAuth();
const platformsStore = storePlatforms();
const collectionsStore = storeCollections();
const emitter = inject<Emitter<Events>>("emitter");
emitter?.on("refreshDrawer", async () => {
const { data: platformData } = await platformApi.getPlatforms();
platformsStore.set(platformData);
});
// Functions
onBeforeMount(async () => {
await api.get("/heartbeat").then(async ({ data: data }) => {
heartbeat.set(data);
if (heartbeat.value.SHOW_SETUP_WIZARD) {
router.push({ name: "setup" });
} else {
await userApi
.fetchCurrentUser()
.then(({ data: user }) => {
auth.setUser(user);
})
.catch((error) => {
console.error(error);
});
await api.get("/config").then(({ data: data }) => {
configStore.set(data);
});
}
});
await platformApi
.getPlatforms()
.then(({ data: platforms }) => {
platformsStore.set(platforms);
})
.catch((error) => {
console.error(error);
});
await collectionApi
.getCollections()
.then(({ data: collections }) => {
collectionsStore.set(collections);
collectionsStore.setFavCollection(
collections.find(
(collection) => collection.name.toLowerCase() === "favourites",
),
);
})
.catch((error) => {
console.error(error);
});
navigationStore.resetDrawers();
});
</script>
<template>
<notification />
<main-app-bar />
<view-loader />
<router-view />
<match-rom-dialog />
<edit-rom-dialog />
<search-cover-dialog />
<add-roms-to-collection-dialog />
<remove-roms-from-collection-dialog />
<delete-rom-dialog />
<edit-user-dialog />
<new-version-dialog />
<upload-progress />
</template>

View File

@@ -0,0 +1,8 @@
<script setup lang="ts">
import RFooter from "@/components/Settings/Footer.vue";
</script>
<template>
<router-view />
<r-footer />
</template>

View File

@@ -1,122 +0,0 @@
<script setup lang="ts">
import InterfaceOption from "@/components/Settings/InterfaceOption.vue";
import RSection from "@/components/common/RSection.vue";
import { isNull } from "lodash";
import { computed, ref } from "vue";
// Initializing refs from localStorage
const storedGroupRoms = localStorage.getItem("settings.groupRoms");
const groupRomsRef = ref(
isNull(storedGroupRoms) ? true : storedGroupRoms === "true",
);
const storedSiblings = localStorage.getItem("settings.showSiblings");
const siblingsRef = ref(
isNull(storedSiblings) ? true : storedSiblings === "true",
);
const storedRegions = localStorage.getItem("settings.showRegions");
const regionsRef = ref(isNull(storedRegions) ? true : storedRegions === "true");
const storedLanguages = localStorage.getItem("settings.showLanguages");
const languagesRef = ref(
isNull(storedLanguages) ? true : storedLanguages === "true",
);
const storedStatus = localStorage.getItem("settings.showStatus");
const statusRef = ref(isNull(storedStatus) ? true : storedStatus === "true");
// Functions to update localStorage
const toggleGroupRoms = (value: boolean) => {
groupRomsRef.value = value;
localStorage.setItem("settings.groupRoms", value.toString());
};
const toggleSiblings = (value: boolean) => {
siblingsRef.value = value;
localStorage.setItem("settings.showSiblings", value.toString());
};
const toggleRegions = (value: boolean) => {
regionsRef.value = value;
localStorage.setItem("settings.showRegions", value.toString());
};
const toggleLanguages = (value: boolean) => {
languagesRef.value = value;
localStorage.setItem("settings.showLanguages", value.toString());
};
const toggleStatus = (value: boolean) => {
statusRef.value = value;
localStorage.setItem("settings.showStatus", value.toString());
};
const options = computed(() => [
{
title: "Group roms",
description: "Group versions of the same rom together in the gallery",
iconEnabled: "mdi-group",
iconDisabled: "mdi-ungroup",
model: groupRomsRef,
modelTrigger: toggleGroupRoms,
},
{
title: "Show siblings",
description:
'Show siblings count in the gallery when "Group roms" option is enabled',
iconEnabled: "mdi-account-group-outline",
iconDisabled: "mdi-account-outline",
model: siblingsRef,
disabled: !groupRomsRef.value,
modelTrigger: toggleSiblings,
},
{
title: "Show regions",
description: "Show region flags in the gallery",
iconEnabled: "mdi-flag-outline",
iconDisabled: "mdi-flag-off-outline",
model: regionsRef,
modelTrigger: toggleRegions,
},
{
title: "Show languages",
description: "Show language flags in the gallery",
iconEnabled: "mdi-flag-outline",
iconDisabled: "mdi-flag-off-outline",
model: languagesRef,
modelTrigger: toggleLanguages,
},
{
title: "Show status",
description:
"Show status icons in the gallery (backlogged, playing, completed, etc)",
iconEnabled: "mdi-check-circle-outline",
iconDisabled: "mdi-close-circle-outline",
model: statusRef,
modelTrigger: toggleStatus,
},
]);
</script>
<template>
<r-section icon="mdi-palette-swatch-outline" title="Interface">
<template #content>
<v-row no-gutters>
<v-col cols="12" md="6" v-for="option in options" :key="option.title">
<interface-option
class="mx-2"
:disabled="option.disabled"
:title="option.title"
:description="option.description"
:icon="
option.model.value ? option.iconEnabled : option.iconDisabled
"
v-model="option.model.value"
@update:model-value="option.modelTrigger"
/>
</v-col>
</v-row>
</template>
</r-section>
</template>

View File

@@ -1,17 +1,14 @@
import { registerPlugins } from "@/plugins";
import "@/styles/common.css";
import "@/styles/fonts.css";
import "@/styles/scrollbar.css";
import "@/styles/common.css";
import type { Events } from "@/types/emitter";
import mitt from "mitt";
import { createApp } from "vue";
import App from "./App.vue";
import App from "@/RomM.vue";
const emitter = mitt<Events>();
const app = createApp(App);
registerPlugins(app);
app.provide("emitter", emitter);
app.mount("#app");

View File

@@ -4,74 +4,106 @@ import storeHeartbeat from "@/stores/heartbeat";
const routes = [
{
path: "/login",
name: "login",
component: () => import("@/views/Login.vue"),
path: "/setup",
name: "setupView",
component: () => import("@/layouts/Auth.vue"),
children: [
{
path: "",
name: "setup",
component: () => import("@/views/Auth/Setup.vue"),
},
],
},
{
path: "/setup",
name: "setup",
component: () => import("@/views/Setup.vue"),
path: "/login",
name: "loginView",
component: () => import("@/layouts/Auth.vue"),
children: [
{
path: "",
name: "login",
component: () => import("@/views/Auth/Login.vue"),
},
],
},
{
path: "/",
name: "home",
component: () => import("@/views/Home.vue"),
name: "main",
component: () => import("@/layouts/Main.vue"),
children: [
{
path: "/",
name: "dashboard",
component: () => import("@/views/Dashboard.vue"),
path: "",
name: "home",
component: () => import("@/views/Home.vue"),
},
{
path: "/platform/:platform",
path: "platform/:platform",
name: "platform",
component: () => import("@/views/Gallery/Platform.vue"),
},
{
path: "/collection/:collection",
path: "collection/:collection",
name: "collection",
component: () => import("@/views/Gallery/Collection.vue"),
},
{
path: "/rom/:rom",
path: "rom/:rom",
name: "rom",
component: () => import("@/views/GameDetails.vue"),
},
{
path: "/rom/:rom/ejs",
path: "rom/:rom/ejs",
name: "emulatorjs",
component: () => import("@/views/EmulatorJS/Base.vue"),
component: () => import("@/views/Player/EmulatorJS/Base.vue"),
},
{
path: "/rom/:rom/ruffle",
path: "rom/:rom/ruffle",
name: "ruffle",
component: () => import("@/views/RuffleRS/Base.vue"),
component: () => import("@/views/Player/RuffleRS/Base.vue"),
},
{
path: "/scan",
path: "scan",
name: "scan",
component: () => import("@/views/Scan.vue"),
},
{
path: "/management",
name: "management",
component: () => import("@/views/Management.vue"),
path: "/user-interface",
component: () => import("@/layouts/Settings.vue"),
children: [
{
path: "",
name: "userInterface",
component: () => import("@/views/Settings/UserInterface.vue"),
},
],
},
{
path: "/settings",
name: "settings",
component: () => import("@/views/Settings.vue"),
path: "/library-management",
component: () => import("@/layouts/Settings.vue"),
children: [
{
path: "",
name: "libraryManagement",
component: () => import("@/views/Settings/LibraryManagement.vue"),
},
],
},
{
path: "/administration",
name: "administration",
component: () => import("@/views/Administration.vue"),
component: () => import("@/layouts/Settings.vue"),
children: [
{
path: "",
name: "administration",
component: () => import("@/views/Settings/Administration.vue"),
},
],
},
{
path: "/:pathMatch(.*)*",
path: ":pathMatch(.*)*",
name: "noMatch",
component: () => import("@/views/Dashboard.vue"),
component: () => import("@/views/Home.vue"),
},
],
},
@@ -82,17 +114,17 @@ const router = createRouter({
routes,
});
router.beforeEach((to, _from, next) => {
router.beforeEach(async (to, _from, next) => {
const heartbeat = storeHeartbeat();
if (to.name == "setup" && !heartbeat.value.SHOW_SETUP_WIZARD) {
next({ name: "dashboard" });
next({ name: "home" });
} else {
next();
}
});
router.afterEach(() => {
// Scroll to top to avoid annoying behaviour in mobile
// Scroll to top to avoid annoying behaviour on mobile
window.scrollTo({ top: 0, left: 0 });
});

View File

@@ -6,6 +6,7 @@ export default defineStore("navigation", {
activeCollectionsDrawer: false,
activeSettingsDrawer: false,
activePlatformInfoDrawer: false,
activeCollectionInfoDrawer: false,
}),
actions: {
@@ -25,9 +26,13 @@ export default defineStore("navigation", {
this.resetDrawersExcept("activePlatformInfoDrawer");
this.activePlatformInfoDrawer = !this.activePlatformInfoDrawer;
},
swtichActiveCollectionInfoDrawer() {
this.resetDrawersExcept("activeCollectionInfoDrawer");
this.activeCollectionInfoDrawer = !this.activeCollectionInfoDrawer;
},
goHome() {
this.resetDrawers();
this.$router.push({ name: "dashboard" });
this.$router.push({ name: "home" });
},
goScan() {
this.resetDrawers();
@@ -38,6 +43,7 @@ export default defineStore("navigation", {
this.activeCollectionsDrawer = false;
this.activeSettingsDrawer = false;
this.activePlatformInfoDrawer = false;
this.activeCollectionInfoDrawer = false;
},
resetDrawersExcept(drawer: string) {
this.activePlatformsDrawer =
@@ -52,6 +58,10 @@ export default defineStore("navigation", {
drawer === "activePlatformInfoDrawer"
? this.activePlatformInfoDrawer
: false;
this.activeCollectionInfoDrawer =
drawer === "activeCollectionInfoDrawer"
? this.activeCollectionInfoDrawer
: false;
},
},
});

View File

@@ -8,6 +8,9 @@ body {
1px 1px 3px #000000,
0 0 3px #000000 !important;
}
.v-progress-linear {
z-index: 9999 !important;
}
.translucent {
backdrop-filter: blur(3px) !important;
text-shadow:
@@ -66,14 +69,3 @@ body {
#main-app-bar ~ #gallery-app-bar {
top: 45px !important;
}
/*
Calculation to fix 100dvh:
main-app-bar -> 45px
gallery-app-bar -> 48px
*/
.fill-height-desktop {
height: calc(100dvh - 48px) !important;
}
.fill-height-mobile {
height: calc(100dvh - 45px - 48px) !important;
}

View File

@@ -4,11 +4,6 @@ import type { Platform } from "@/stores/platforms";
import type { SimpleRom } from "@/stores/roms";
import type { User } from "@/stores/users";
export type UserItem = User & {
password: string;
avatar?: File;
};
export type SnackbarStatus = {
id?: number;
msg: string;
@@ -25,7 +20,7 @@ export type Events = {
showRemoveFromCollectionDialog: SimpleRom[];
showDeleteCollectionDialog: Collection;
showMatchRomDialog: SimpleRom;
showSearchCoverDialog: string;
showSearchCoverDialog: { term: string; aspectRatio: number | null };
updateUrlCover: string;
showSearchRomDialog: null;
showEditRomDialog: SimpleRom;

4
frontend/src/types/user.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export type UserItem = User & {
password: string;
avatar?: File;
};

View File

@@ -3,6 +3,9 @@ import type { SimpleRom } from "@/stores/roms";
import type { Heartbeat } from "@/stores/heartbeat";
import type { RomUserStatus } from "@/__generated__";
/**
* Views configuration object.
*/
export const views: Record<
number,
{
@@ -44,8 +47,17 @@ export const views: Record<
},
};
/**
* Default path for user avatars.
*/
export const defaultAvatarPath = "/assets/default/user.png";
/**
* Normalize a string by converting it to lowercase and removing diacritics.
*
* @param s The string to normalize.
* @returns The normalized string.
*/
export function normalizeString(s: string) {
return s
.toLowerCase()
@@ -53,6 +65,12 @@ export function normalizeString(s: string) {
.replace(/[\u0300-\u036f]/g, "");
}
/**
* Convert a cron expression to a human-readable string.
*
* @param expression The cron expression to convert.
* @returns The human-readable string.
*/
export function convertCronExperssion(expression: string) {
let convertedExpression = cronstrue.toString(expression, { verbose: true });
convertedExpression =
@@ -61,6 +79,13 @@ export function convertCronExperssion(expression: string) {
return convertedExpression;
}
/**
* Generate a download link for ROM content.
*
* @param rom The ROM object.
* @param files Optional array of file names to include in the download.
* @returns The download link.
*/
export function getDownloadLink({
rom,
files = [],
@@ -82,8 +107,7 @@ export function getDownloadLink({
*
* @param bytes Number of bytes.
* @param decimals Number of decimal places to display.
*
* @return Formatted string.
* @returns Formatted string.
*/
export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return "0 Bytes";
@@ -95,11 +119,10 @@ export function formatBytes(bytes: number, decimals = 2) {
}
/**
* Format a timestamp to a human-readable string.
*
* Format timestamp to human-readable text
*
* @param string timestamp
* @returns string Formatted timestamp
* @param timestamp The timestamp to format.
* @returns The formatted timestamp.
*/
export function formatTimestamp(timestamp: string | null) {
if (!timestamp) return "-";
@@ -108,6 +131,12 @@ export function formatTimestamp(timestamp: string | null) {
return date.toLocaleString("en-GB");
}
/**
* Convert a region code to an emoji.
*
* @param region The region code.
* @returns The corresponding emoji.
*/
export function regionToEmoji(region: string) {
switch (region.toLowerCase()) {
case "as":
@@ -203,6 +232,12 @@ export function regionToEmoji(region: string) {
}
}
/**
* Convert a language code to an emoji.
*
* @param language The language code.
* @returns The corresponding emoji.
*/
export function languageToEmoji(language: string) {
switch (language.toLowerCase()) {
case "ar":
@@ -264,6 +299,9 @@ export function languageToEmoji(language: string) {
}
}
/**
* Map of supported EJS cores for each platform.
*/
const _EJS_CORES_MAP = {
"3do": ["opera"],
amiga: ["puae"],
@@ -343,10 +381,23 @@ const _EJS_CORES_MAP = {
export type EJSPlatformSlug = keyof typeof _EJS_CORES_MAP;
/**
* Get the supported EJS cores for a given platform.
*
* @param platformSlug The platform slug.
* @returns An array of supported cores.
*/
export function getSupportedEJSCores(platformSlug: string) {
return _EJS_CORES_MAP[platformSlug.toLowerCase() as EJSPlatformSlug] || [];
}
/**
* Check if EJS emulation is supported for a given platform.
*
* @param platformSlug The platform slug.
* @param heartbeat The heartbeat object.
* @returns True if supported, false otherwise.
*/
export function isEJSEmulationSupported(
platformSlug: string,
heartbeat: Heartbeat,
@@ -357,6 +408,13 @@ export function isEJSEmulationSupported(
);
}
/**
* Check if Ruffle emulation is supported for a given platform.
*
* @param platformSlug The platform slug.
* @param heartbeat The heartbeat object.
* @returns True if supported, false otherwise.
*/
export function isRuffleEmulationSupported(
platformSlug: string,
heartbeat: Heartbeat,
@@ -369,6 +427,9 @@ export function isRuffleEmulationSupported(
type PlayingStatus = RomUserStatus | "backlogged" | "now_playing" | "hidden";
/**
* Array of difficulty emojis.
*/
export const difficultyEmojis = [
"😴",
"🥱",
@@ -382,6 +443,9 @@ export const difficultyEmojis = [
"😵",
];
/**
* Map of ROM statuses to their corresponding emoji and text.
*/
export const romStatusMap: Record<
PlayingStatus,
{ emoji: string; text: string }
@@ -396,10 +460,19 @@ export const romStatusMap: Record<
hidden: { emoji: "👻", text: "Hidden" },
};
/**
* Inverse map of ROM statuses from text to status key.
*/
const inverseRomStatusMap = Object.fromEntries(
Object.entries(romStatusMap).map(([key, value]) => [value.text, key]),
) as Record<string, PlayingStatus>;
/**
* Get the emoji for a given ROM status.
*
* @param status The ROM status.
* @returns The corresponding emoji.
*/
export function getEmojiForStatus(status: PlayingStatus) {
if (status) {
return romStatusMap[status].emoji;
@@ -408,6 +481,12 @@ export function getEmojiForStatus(status: PlayingStatus) {
}
}
/**
* Get the text for a given ROM status.
*
* @param status The ROM status.
* @returns The corresponding text.
*/
export function getTextForStatus(status: PlayingStatus) {
if (status) {
return romStatusMap[status].text;
@@ -416,6 +495,12 @@ export function getTextForStatus(status: PlayingStatus) {
}
}
/**
* Get the status key for a given text.
*
* @param text The text to convert.
* @returns The corresponding status key.
*/
export function getStatusKeyForText(text: string) {
return inverseRomStatusMap[text];
}

View File

@@ -0,0 +1,134 @@
<script setup lang="ts">
import identityApi from "@/services/api/identity";
import { refetchCSRFToken } from "@/services/api/index";
import storeHeartbeat from "@/stores/heartbeat";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, ref } from "vue";
import { useRouter } from "vue-router";
// Props
const heartbeatStore = storeHeartbeat();
const emitter = inject<Emitter<Events>>("emitter");
const router = useRouter();
const username = ref("");
const password = ref("");
const visiblePassword = ref(false);
const loggingIn = ref(false);
// Functions
async function login() {
loggingIn.value = true;
await identityApi
.login(username.value, password.value)
.then(async () => {
await refetchCSRFToken();
const params = new URLSearchParams(window.location.search);
router.push(params.get("next") ?? "/");
})
.catch(({ response, message }) => {
const errorMessage =
response.data?.detail ||
response.data ||
message ||
response.statusText;
emitter?.emit("snackbarShow", {
msg: `Unable to login: ${errorMessage}`,
icon: "mdi-close-circle",
color: "red",
});
console.error(
`[${response.status} ${response.statusText}] ${errorMessage}`,
);
})
.finally(() => {
loggingIn.value = false;
});
}
async function loginOIDC() {
loggingIn.value = true;
window.open("/api/login/openid", "_self");
}
</script>
<template>
<v-card class="translucent-dark py-8 px-5" width="500">
<v-img src="/assets/isotipo.svg" class="mx-auto" width="150" />
<v-row class="text-white justify-center mt-2" no-gutters>
<v-col cols="10">
<v-form @submit.prevent="login">
<v-text-field
v-model="username"
label="Username"
type="text"
required
autocomplete="on"
prepend-inner-icon="mdi-account"
variant="underlined"
/>
<v-text-field
v-model="password"
label="Password"
:type="visiblePassword ? 'text' : 'password'"
required
autocomplete="on"
prepend-inner-icon="mdi-lock"
:append-inner-icon="visiblePassword ? 'mdi-eye-off' : 'mdi-eye'"
@click:append-inner="visiblePassword = !visiblePassword"
variant="underlined"
/>
<v-btn
type="submit"
class="bg-terciary"
block
:loading="loggingIn"
:disabled="loggingIn || !username || !password"
:variant="!username || !password ? 'text' : 'flat'"
>
<span>Login</span>
<template #append>
<v-icon class="text-romm-accent-1"
>mdi-chevron-right-circle-outline</v-icon
>
</template>
<template #loader>
<v-progress-circular
color="romm-accent-1"
:width="2"
:size="20"
indeterminate
/>
</template>
</v-btn>
<v-btn
block
type="submit"
v-if="heartbeatStore.value.OIDC.ENABLED"
:disabled="loggingIn"
:loading="loggingIn"
:variant="'text'"
class="bg-terciary"
@click="loginOIDC()"
>
<span>Login with OIDC</span>
<template #append>
<v-icon class="text-romm-accent-1"
>mdi-chevron-right-circle-outline</v-icon
>
</template>
<template #loader>
<v-progress-circular
color="romm-accent-1"
:width="2"
:size="20"
indeterminate
/>
</template>
</v-btn>
</v-form>
</v-col>
</v-row>
</v-card>
</template>

View File

@@ -0,0 +1,191 @@
<script setup lang="ts">
import router from "@/plugins/router";
import { refetchCSRFToken } from "@/services/api/index";
import userApi from "@/services/api/user";
import storeHeartbeat from "@/stores/heartbeat";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { computed, inject, ref } from "vue";
import { useDisplay } from "vuetify";
// Props
const { xs } = useDisplay();
const emitter = inject<Emitter<Events>>("emitter");
const heartbeat = storeHeartbeat();
const visiblePassword = ref(false);
// Use a computed property to reactively update metadataOptions based on heartbeat
const metadataOptions = computed(() => [
{
name: "IGDB",
value: "igdb",
logo_path: "/assets/scrappers/igdb.png",
disabled: !heartbeat.value.METADATA_SOURCES?.IGDB_API_ENABLED,
},
{
name: "MobyGames",
value: "moby",
logo_path: "/assets/scrappers/moby.png",
disabled: !heartbeat.value.METADATA_SOURCES?.MOBY_API_ENABLED,
},
{
name: "SteamgridDB",
value: "sgdb",
logo_path: "/assets/scrappers/sgdb.png",
disabled: !heartbeat.value.METADATA_SOURCES?.STEAMGRIDDB_ENABLED,
},
]);
const defaultAdminUser = ref({
username: "",
password: "",
role: "admin",
});
const step = ref(1); // 1: Create admin user, 2: Check metadata sources, 3: Finish
const filledAdminUser = computed(
() =>
defaultAdminUser.value.username != "" &&
defaultAdminUser.value.password != "",
);
const isFirstStep = computed(() => step.value == 1);
const isLastStep = computed(() => step.value == 2);
// Functions
async function finishWizard() {
await userApi
.createUser(defaultAdminUser.value)
.then(async () => {
await refetchCSRFToken();
router.push({ name: "login" });
})
.catch(({ response, message }) => {
emitter?.emit("snackbarShow", {
msg: `Unable to create user: ${
response?.data?.detail || response?.statusText || message
}`,
icon: "mdi-close-circle",
color: "red",
});
});
}
</script>
<template>
<v-card class="translucent-dark px-3" width="700">
<v-img src="/assets/isotipo.svg" class="mx-auto mt-6" width="70" />
<v-stepper :mobile="xs" class="bg-transparent" v-model="step" flat>
<template #default="{ prev, next }">
<v-stepper-header>
<v-stepper-item :value="1">
<template #title>
<span class="text-white text-shadow">Create an admin user</span>
</template>
</v-stepper-item>
<v-divider></v-divider>
<v-stepper-item :value="2">
<template #title>
<span class="text-white text-shadow">Check metadata sources</span>
</template>
</v-stepper-item>
</v-stepper-header>
<v-stepper-window>
<v-stepper-window-item :value="1" :key="1">
<v-row no-gutters>
<v-col>
<v-row v-if="xs" no-gutters class="text-center">
<v-col>
<span>Create an admin user</span>
</v-col>
</v-row>
<v-row class="text-white justify-center mt-3" no-gutters>
<v-col cols="10" md="8">
<v-form @submit.prevent>
<v-text-field
v-model="defaultAdminUser.username"
label="Username"
type="text"
required
autocomplete="on"
prepend-inner-icon="mdi-account"
variant="underlined"
/>
<v-text-field
v-model="defaultAdminUser.password"
label="Password"
:type="visiblePassword ? 'text' : 'password'"
required
autocomplete="on"
prepend-inner-icon="mdi-lock"
:append-inner-icon="
visiblePassword ? 'mdi-eye-off' : 'mdi-eye'
"
@click:append-inner="visiblePassword = !visiblePassword"
variant="underlined"
/>
</v-form>
</v-col>
</v-row>
</v-col>
</v-row>
</v-stepper-window-item>
<v-stepper-window-item :value="2" :key="2">
<v-row no-gutters>
<v-col>
<v-row v-if="xs" no-gutters class="text-center mb-6">
<v-col>
<span>Check metadata sources</span>
</v-col>
</v-row>
<v-row class="justify-center align-center" no-gutters>
<v-col :max-width="300" id="sources">
<v-list-item
v-for="source in metadataOptions"
class="text-white text-shadow"
:title="source.name"
:subtitle="
source.disabled ? 'API key missing or invalid' : ''
"
>
<template #prepend>
<v-avatar size="30" rounded="1">
<v-img :src="source.logo_path" />
</v-avatar>
</template>
<template #append>
<span class="ml-2" v-if="source.disabled"></span>
<span class="ml-2" v-else></span>
</template>
</v-list-item>
</v-col>
</v-row>
</v-col>
</v-row>
</v-stepper-window-item>
</v-stepper-window>
<v-stepper-actions :disabled="!filledAdminUser">
<template #prev>
<v-btn
class="text-white text-shadow"
:ripple="false"
:disabled="isFirstStep"
@click="prev"
>
{{ isFirstStep ? "" : "previous" }}
</v-btn>
</template>
<template #next>
<v-btn
class="text-white text-shadow"
@click="!isLastStep ? next() : finishWizard()"
>
{{ !isLastStep ? "Next" : "Finish" }}
</v-btn>
</template>
</v-stepper-actions>
</template>
</v-stepper>
</v-card>
</template>

View File

@@ -1,39 +0,0 @@
<script setup lang="ts">
import Collections from "@/layouts/Dashboard/Collections.vue";
import Platforms from "@/layouts/Dashboard/Platforms.vue";
import recentlyAdded from "@/layouts/Dashboard/Recent.vue";
import Stats from "@/layouts/Dashboard/Stats.vue";
import romApi from "@/services/api/rom";
import storeCollections from "@/stores/collections";
import storePlatforms from "@/stores/platforms";
import storeRoms from "@/stores/roms";
import { storeToRefs } from "pinia";
import { onMounted } from "vue";
// Props
const romsStore = storeRoms();
const { recentRoms } = storeToRefs(romsStore);
const platforms = storePlatforms();
const { filledPlatforms } = storeToRefs(platforms);
const collections = storeCollections();
const { allCollections } = storeToRefs(collections);
// Functions
onMounted(async () => {
await romApi
.getRecentRoms()
.then(({ data: recentData }) => {
romsStore.setRecentRoms(recentData);
})
.catch((error) => {
console.error(error);
});
});
</script>
<template>
<stats />
<recently-added v-if="recentRoms.length > 0" />
<platforms v-if="filledPlatforms.length > 0" />
<collections v-if="allCollections.length > 0" />
</template>

View File

@@ -287,16 +287,11 @@ onBeforeUnmount(() => {
<template v-if="!noCollectionError">
<gallery-app-bar-collection />
<template v-if="filteredRoms.length > 0">
<v-row
no-gutters
class="overflow-hidden align-center"
:class="{ 'pa-1': currentView != 2 }"
>
<v-row v-show="currentView != 2" class="pa-1" no-gutters>
<!-- Gallery cards view -->
<!-- v-show instead of v-if to avoid recalculate on view change -->
<v-col
v-for="rom in filteredRoms.slice(0, itemsShown)"
v-show="currentView != 2"
:key="rom.id"
class="pa-1 align-self-end"
:cols="views[currentView]['size-cols']"
@@ -309,13 +304,13 @@ onBeforeUnmount(() => {
:key="rom.updated_at"
:rom="rom"
title-on-hover
pointerOnHover
withLink
pointer-on-hover
with-link
show-flags
show-action-bar
show-fav
transform-scale
with-border
show-fav
show-platform-icon
:with-border-romm-accent="
romsStore.isSimpleRom(rom) && selectedRoms?.includes(rom)
@@ -325,16 +320,11 @@ onBeforeUnmount(() => {
@touchend="onGameTouchEnd"
/>
</v-col>
</v-row>
<!-- Gallery list view -->
<v-col v-show="currentView == 2">
<game-data-table
:class="{
'fill-height-desktop': !smAndDown,
'fill-height-mobile': smAndDown,
}"
/>
</v-col>
<!-- Gallery list view -->
<v-row v-show="currentView == 2" class="h-100" no-gutters>
<game-data-table class="fill-height" />
</v-row>
<fab-overlay />
</template>

View File

@@ -43,6 +43,7 @@ let timeout: ReturnType<typeof setTimeout>;
const emitter = inject<Emitter<Events>>("emitter");
emitter?.on("filter", onFilterChange);
// Functions
async function fetchRoms() {
if (gettingRoms.value) return;
@@ -303,16 +304,11 @@ onBeforeUnmount(() => {
<template v-if="!noPlatformError">
<gallery-app-bar />
<template v-if="filteredRoms.length > 0">
<v-row
no-gutters
class="overflow-hidden"
:class="{ 'pa-1': currentView != 2 }"
>
<v-row v-show="currentView != 2" class="pa-1" no-gutters>
<!-- Gallery cards view -->
<!-- v-show instead of v-if to avoid recalculate on view change -->
<v-col
v-for="rom in filteredRoms.slice(0, itemsShown)"
v-show="currentView != 2"
:key="rom.id"
class="pa-1 align-self-end"
:cols="views[currentView]['size-cols']"
@@ -326,8 +322,8 @@ onBeforeUnmount(() => {
:key="rom.updated_at"
:rom="rom"
title-on-hover
pointerOnHover
withLink
pointer-on-hover
with-link
show-flags
show-action-bar
show-fav
@@ -341,16 +337,11 @@ onBeforeUnmount(() => {
@touchend="onGameTouchEnd"
/>
</v-col>
</v-row>
<!-- Gallery list view -->
<v-col v-show="currentView == 2">
<game-data-table
:class="{
'fill-height-desktop': !smAndDown,
'fill-height-mobile': smAndDown,
}"
/>
</v-col>
<!-- Gallery list view -->
<v-row v-show="currentView == 2" class="h-100" no-gutters>
<game-data-table class="h-100" />
</v-row>
<fab-overlay />
</template>

View File

@@ -7,7 +7,6 @@ import GameInfo from "@/components/Details/Info/GameInfo.vue";
import Personal from "@/components/Details/Personal.vue";
import RelatedGames from "@/components/Details/RelatedGames.vue";
import Saves from "@/components/Details/Saves.vue";
import storePlatforms from "@/stores/platforms";
import States from "@/components/Details/States.vue";
import TitleInfo from "@/components/Details/Title.vue";
import EmptyGame from "@/components/common/EmptyGame.vue";
@@ -37,7 +36,6 @@ const { smAndDown, mdAndDown, mdAndUp, lgAndUp } = useDisplay();
const emitter = inject<Emitter<Events>>("emitter");
const noRomError = ref(false);
const romsStore = storeRoms();
const platfotmsStore = storePlatforms();
const { currentRom, gettingRoms } = storeToRefs(romsStore);
async function fetchDetails() {
@@ -89,63 +87,36 @@ watch(
<template v-if="currentRom && !gettingRoms">
<background-header />
<v-row
class="px-5"
:class="{
'ml-6': mdAndUp,
'justify-center': smAndDown,
}"
no-gutters
>
<v-col
class="cover"
:class="{
'cover-desktop': mdAndUp,
}"
:style="
smAndDown
? platfotmsStore.getAspectRatio(currentRom.platform_id) == 1 / 1
? 'margin-top: -220px;'
: 'margin-top: -280px;'
: ''
"
>
<game-card :key="currentRom.updated_at" :rom="currentRom" />
<action-bar class="mt-2" :rom="currentRom" />
<related-games v-if="mdAndUp" class="mt-3" :rom="currentRom" />
<v-row class="px-5" no-gutters :class="{ 'justify-center': smAndDown }">
<v-col cols="auto">
<v-container :width="270" id="artwork-container" class="pa-0">
<game-card
:show-not-identified="false"
:key="currentRom.updated_at"
:rom="currentRom"
/>
<action-bar class="mt-2" :rom="currentRom" />
<related-games v-if="mdAndUp" class="mt-4" :rom="currentRom" />
</v-container>
</v-col>
<v-col
cols="12"
md="8"
class="px-5"
:class="{
'info-lg': mdAndUp,
}"
:style="
smAndDown
? platfotmsStore.getAspectRatio(currentRom.platform_id) == 1 / 1
? 'margin-top: -40px;'
: 'margin-top: 100px;'
: ''
"
>
<v-col>
<div
class="px-3 pb-3"
:class="{
'position-absolute title-desktop': mdAndUp,
'justify-center': smAndDown,
}"
class="ml-4"
:class="{ 'position-absolute title-desktop': mdAndUp }"
>
<title-info :rom="currentRom" />
</div>
<v-row
:class="{
'justify-center': smAndDown,
}"
:class="{ 'px-4': mdAndUp, 'justify-center': smAndDown }"
no-gutters
>
<v-tabs v-model="tab" slider-color="romm-accent-1" rounded="0">
<v-tabs
v-model="tab"
slider-color="romm-accent-1"
:class="{ 'mt-4': smAndDown }"
rounded="0"
>
<v-tab value="details" rounded="0"> Details </v-tab>
<v-tab value="saves" rounded="0"> Saves </v-tab>
<v-tab value="states" rounded="0"> States </v-tab>
@@ -176,12 +147,10 @@ watch(
Related Games
</v-tab>
</v-tabs>
</v-row>
<v-row no-gutters class="mb-4">
<v-col cols="12">
<v-col cols="12" class="px-2">
<v-window disabled v-model="tab" class="py-2">
<v-window-item value="details">
<v-row no-gutters :class="{ 'mx-2': mdAndUp }">
<v-row no-gutters>
<v-col>
<file-info :rom="currentRom" />
<game-info :rom="currentRom" />
@@ -227,11 +196,11 @@ watch(
</v-row>
</v-col>
<template v-if="lgAndUp">
<v-col>
<additional-content :rom="currentRom" />
</v-col>
</template>
<v-col cols="auto" v-if="lgAndUp">
<v-container :width="270" class="pa-0">
<additional-content class="mt-2" :rom="currentRom" />
</v-container>
</v-col>
</v-row>
</template>
@@ -239,20 +208,10 @@ watch(
</template>
<style scoped>
.cover {
min-width: 270px;
min-height: 360px;
max-width: 270px;
max-height: 360px;
}
.cover-desktop {
margin-top: -230px;
}
.title-desktop {
margin-top: -190px;
margin-left: -20px;
}
.info-mobile {
margin-top: 100px;
#artwork-container {
margin-top: -230px;
}
</style>

View File

@@ -1,122 +1,49 @@
<script setup lang="ts">
import CreateUserDialog from "@/components/Administration/Users/Dialog/CreateUser.vue";
import DeleteUserDialog from "@/components/Administration/Users/Dialog/DeleteUser.vue";
import EditUserDialog from "@/components/Administration/Users/Dialog/EditUser.vue";
import CreateExclusionDialog from "@/components/Management/Dialog/CreateExclusion.vue";
import CreatePlatformBindingDialog from "@/components/Management/Dialog/CreatePlatformBinding.vue";
import CreatePlatformVersionDialog from "@/components/Management/Dialog/CreatePlatformVersion.vue";
import DeletePlatformBindingDialog from "@/components/Management/Dialog/DeletePlatformBinding.vue";
import DeletePlatformVersionDialog from "@/components/Management/Dialog/DeletePlatformVersion.vue";
import AddRomsToCollectionDialog from "@/components/common/Collection/Dialog/AddRoms.vue";
import CreateCollectionDialog from "@/components/common/Collection/Dialog/CreateCollection.vue";
import DeleteCollectionDialog from "@/components/common/Collection/Dialog/DeleteCollection.vue";
import EditCollectionDialog from "@/components/common/Collection/Dialog/EditCollection.vue";
import RemoveRomsFromCollectionDialog from "@/components/common/Collection/Dialog/RemoveRoms.vue";
import DeleteAssetDialog from "@/components/common/Game/Dialog/Asset/DeleteAssets.vue";
import CopyRomDownloadLinkDialog from "@/components/common/Game/Dialog/CopyDownloadLink.vue";
import DeleteRomDialog from "@/components/common/Game/Dialog/DeleteRom.vue";
import EditRomDialog from "@/components/common/Game/Dialog/EditRom.vue";
import MatchRomDialog from "@/components/common/Game/Dialog/MatchRom.vue";
import SearchRomDialog from "@/components/common/Game/Dialog/SearchRom.vue";
import UploadRomDialog from "@/components/common/Game/Dialog/UploadRom.vue";
import LoadingView from "@/components/common/LoadingView.vue";
import CollectionsDrawer from "@/components/common/Navigation/CollectionsDrawer.vue";
import MainAppBar from "@/components/common/Navigation/MainAppBar.vue";
import MainDrawer from "@/components/common/Navigation/MainDrawer.vue";
import PlatformsDrawer from "@/components/common/Navigation/PlatformsDrawer.vue";
import SettingsDrawer from "@/components/common/Navigation/SettingsDrawer.vue";
import NewVersion from "@/components/common/NewVersion.vue";
import DeletePlatformDialog from "@/components/common/Platform/Dialog/DeletePlatform.vue";
import SearchCoverDialog from "@/components/common/SearchCover.vue";
import collectionApi from "@/services/api/collection";
import platformApi from "@/services/api/platform";
import storeAuth from "@/stores/auth";
import Collections from "@/components/Home/Collections.vue";
import Platforms from "@/components/Home/Platforms.vue";
import RecentlyAdded from "@/components/Home/Recent.vue";
import Stats from "@/components/Home/Stats.vue";
import romApi from "@/services/api/rom";
import storeCollections from "@/stores/collections";
import storeNavigation from "@/stores/navigation";
import storePlatforms from "@/stores/platforms";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, onBeforeMount } from "vue";
import { useDisplay } from "vuetify";
import storeRoms from "@/stores/roms";
import { storeToRefs } from "pinia";
import { onMounted } from "vue";
import { isNull } from "lodash";
// Props
const { smAndDown } = useDisplay();
const navigationStore = storeNavigation();
const auth = storeAuth();
const platformsStore = storePlatforms();
const collectionsStore = storeCollections();
const emitter = inject<Emitter<Events>>("emitter");
emitter?.on("refreshDrawer", async () => {
const { data: platformData } = await platformApi.getPlatforms();
platformsStore.set(platformData);
});
const romsStore = storeRoms();
const { recentRoms } = storeToRefs(romsStore);
const platforms = storePlatforms();
const { filledPlatforms } = storeToRefs(platforms);
const collections = storeCollections();
const { allCollections } = storeToRefs(collections);
const showRecentRoms = isNull(localStorage.getItem("settings.showRecentRoms"))
? true
: localStorage.getItem("settings.showRecentRoms") === "true";
const showPlatforms = isNull(localStorage.getItem("settings.showPlatforms"))
? true
: localStorage.getItem("settings.showPlatforms") === "true";
const showCollections = isNull(localStorage.getItem("settings.showCollections"))
? true
: localStorage.getItem("settings.showCollections") === "true";
onBeforeMount(async () => {
await platformApi
.getPlatforms()
.then(({ data: platforms }) => {
platformsStore.set(platforms);
// Functions
onMounted(async () => {
await romApi
.getRecentRoms()
.then(({ data: recentData }) => {
romsStore.setRecentRoms(recentData);
})
.catch((error) => {
console.error(error);
});
await collectionApi
.getCollections()
.then(({ data: collections }) => {
collectionsStore.set(collections);
collectionsStore.setFavCollection(
collections.find(
(collection) => collection.name.toLowerCase() === "favourites",
),
);
})
.catch((error) => {
console.error(error);
});
navigationStore.resetDrawers();
});
</script>
<template>
<main-drawer v-if="!smAndDown" />
<main-app-bar v-if="smAndDown" />
<platforms-drawer />
<collections-drawer />
<settings-drawer />
<new-version />
<router-view />
<delete-platform-dialog />
<create-collection-dialog />
<edit-collection-dialog />
<add-roms-to-collection-dialog />
<remove-roms-from-collection-dialog />
<delete-collection-dialog />
<search-rom-dialog />
<match-rom-dialog />
<search-cover-dialog />
<copy-rom-download-link-dialog />
<upload-rom-dialog />
<edit-rom-dialog />
<delete-rom-dialog />
<delete-asset-dialog />
<create-platform-binding-dialog />
<delete-platform-binding-dialog />
<create-platform-version-dialog />
<delete-platform-version-dialog />
<create-exclusion-dialog />
<create-user-dialog />
<edit-user-dialog />
<delete-user-dialog />
<loading-view />
<stats />
<recently-added v-if="recentRoms.length > 0 && showRecentRoms" />
<platforms v-if="filledPlatforms.length > 0 && showPlatforms" />
<collections v-if="allCollections.length > 0 && showCollections" />
</template>
<style scoped>
.v-progress-linear {
z-index: 9999 !important;
}
</style>

View File

@@ -1,171 +0,0 @@
<script setup lang="ts">
import identityApi from "@/services/api/identity";
import { refetchCSRFToken } from "@/services/api/index";
import storeHeartbeat from "@/stores/heartbeat";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, ref } from "vue";
import { useRouter } from "vue-router";
// Props
const heartbeatStore = storeHeartbeat();
const emitter = inject<Emitter<Events>>("emitter");
const router = useRouter();
const username = ref("");
const password = ref("");
const visiblePassword = ref(false);
const loggingIn = ref(false);
async function login() {
loggingIn.value = true;
await identityApi
.login(username.value, password.value)
.then(async () => {
// Refetch CSRF token
await refetchCSRFToken();
const params = new URLSearchParams(window.location.search);
router.push(params.get("next") ?? "/");
})
.catch(({ response, message }) => {
const errorMessage =
response.data?.detail ||
response.data ||
message ||
response.statusText;
emitter?.emit("snackbarShow", {
msg: `Unable to login: ${errorMessage}`,
icon: "mdi-close-circle",
color: "red",
});
console.error(
`[${response.status} ${response.statusText}] ${errorMessage}`,
);
})
.finally(() => {
loggingIn.value = false;
});
}
async function loginOIDC() {
loggingIn.value = true;
window.open("/api/login/openid", "_self");
}
</script>
<template>
<span id="bg" />
<v-container class="fill-height justify-center">
<v-card class="translucent-dark py-8 px-5" width="500">
<v-row no-gutters>
<v-col>
<v-img src="/assets/isotipo.svg" class="mx-auto" width="150" />
<v-row class="text-white justify-center mt-2" no-gutters>
<v-col cols="10" md="8">
<v-form @submit.prevent>
<v-text-field
v-model="username"
autocomplete="on"
required
prepend-inner-icon="mdi-account"
type="text"
label="Username"
variant="underlined"
/>
<v-text-field
v-model="password"
autocomplete="on"
required
prepend-inner-icon="mdi-lock"
:type="visiblePassword ? 'text' : 'password'"
label="Password"
variant="underlined"
:append-inner-icon="
visiblePassword ? 'mdi-eye-off' : 'mdi-eye'
"
@click:append-inner="visiblePassword = !visiblePassword"
/>
<v-btn
type="submit"
:disabled="loggingIn || !username || !password"
:variant="!username || !password ? 'text' : 'flat'"
class="bg-terciary"
block
:loading="loggingIn"
@click="login()"
>
<span>Login</span>
<template #append>
<v-icon class="text-romm-accent-1"
>mdi-chevron-right-circle-outline</v-icon
>
</template>
<template #loader>
<v-progress-circular
color="romm-accent-1"
:width="2"
:size="20"
indeterminate
/>
</template>
</v-btn>
<v-btn
block
type="submit"
v-if="heartbeatStore.value.OIDC.ENABLED"
:disabled="loggingIn"
:loading="loggingIn"
:variant="'text'"
class="bg-terciary"
@click="loginOIDC()"
>
<span>Login with OIDC</span>
<template #append>
<v-icon class="text-romm-accent-1"
>mdi-chevron-right-circle-outline</v-icon
>
</template>
<template #loader>
<v-progress-circular
color="romm-accent-1"
:width="2"
:size="20"
indeterminate
/>
</template>
</v-btn>
</v-form>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card>
<div id="version" class="position-absolute">
<span class="text-white text-shadow">{{
heartbeatStore.value.VERSION
}}</span>
</div>
</v-container>
</template>
<style>
#bg {
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
background: url("/assets/login_bg.png") center center;
background-size: cover;
}
#version {
bottom: 0.3rem;
right: 0.5rem;
}
</style>

View File

@@ -1,11 +0,0 @@
<script setup lang="ts">
import Excluded from "@/layouts/Management/Excluded.vue";
import PlatformBinding from "@/layouts/Management/PlatformBinding.vue";
import PlatformVersions from "@/layouts/Management/PlatformVersions.vue";
</script>
<template>
<platform-binding />
<platform-versions />
<excluded />
</template>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { FirmwareSchema, SaveSchema, StateSchema } from "@/__generated__";
import RAvatar from "@/components/common/Game/RAvatar.vue";
import RomListItem from "@/components/common/Game/ListItem.vue";
import firmwareApi from "@/services/api/firmware";
import romApi from "@/services/api/rom";
import storeGalleryView from "@/stores/galleryView";
import type { DetailedRom } from "@/stores/roms";
import { formatBytes, formatTimestamp, getSupportedEJSCores } from "@/utils";
import Player from "@/views/EmulatorJS/Player.vue";
import Player from "@/views/Player/EmulatorJS/Player.vue";
import { isNull } from "lodash";
import { storeToRefs } from "pinia";
import { onMounted, ref } from "vue";
@@ -131,19 +131,7 @@ onMounted(async () => {
src="/assets/emulatorjs/powered_by_emulatorjs.png"
/>
<v-divider class="my-4" />
<v-list-item class="px-2">
<template #prepend>
<r-avatar :rom="rom" />
</template>
<v-row no-gutters
><v-col>{{ rom.name }}</v-col></v-row
>
<v-row no-gutters
><v-col class="text-romm-accent-1">{{
rom.file_name
}}</v-col></v-row
>
</v-list-item>
<rom-list-item :rom="rom" with-filename />
<v-divider class="my-4" />
<v-select
v-if="supportedCores.length > 1"

Some files were not shown because too many files have changed in this diff Show More