mirror of
https://github.com/rommapp/romm.git
synced 2026-02-19 07:50:57 +01:00
Merge branch 'master' into openid-connect
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"languages": {
|
||||
"Python": {
|
||||
"tab_size": 4
|
||||
},
|
||||
"Vue.js": {
|
||||
"tab_size": 2,
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "prettier",
|
||||
"arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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}"),
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
36
docker/init_scripts/docker-entrypoint.sh
Executable file
36
docker/init_scripts/docker-entrypoint.sh
Executable 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 "$@"
|
||||
@@ -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
7
frontend/src/RomM.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main class="h-100">
|
||||
<router-view />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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");
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
258
frontend/src/components/Settings/UserInterface/Interface.vue
Normal file
258
frontend/src/components/Settings/UserInterface/Interface.vue
Normal 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>
|
||||
@@ -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 }"
|
||||
@@ -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>
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
60
frontend/src/components/common/Game/ListItem.vue
Normal file
60
frontend/src/components/common/Game/ListItem.vue
Normal 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>
|
||||
@@ -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">{{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
34
frontend/src/layouts/Auth.vue
Normal file
34
frontend/src/layouts/Auth.vue
Normal 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>
|
||||
107
frontend/src/layouts/Main.vue
Normal file
107
frontend/src/layouts/Main.vue
Normal 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>
|
||||
8
frontend/src/layouts/Settings.vue
Normal file
8
frontend/src/layouts/Settings.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import RFooter from "@/components/Settings/Footer.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
<r-footer />
|
||||
</template>
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
7
frontend/src/types/emitter.d.ts
vendored
7
frontend/src/types/emitter.d.ts
vendored
@@ -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
4
frontend/src/types/user.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export type UserItem = User & {
|
||||
password: string;
|
||||
avatar?: 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];
|
||||
}
|
||||
|
||||
134
frontend/src/views/Auth/Login.vue
Normal file
134
frontend/src/views/Auth/Login.vue
Normal 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>
|
||||
191
frontend/src/views/Auth/Setup.vue
Normal file
191
frontend/src/views/Auth/Setup.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user