mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Switch to Hilla for UI
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
+16
-29
@@ -3,11 +3,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
plugins {
|
||||
id("org.springframework.boot") version "3.2.2"
|
||||
id("io.spring.dependency-management") version "1.1.4"
|
||||
id("com.vaadin") version "24.3.3"
|
||||
id("dev.hilla") version "2.5.6"
|
||||
kotlin("jvm") version "1.9.22"
|
||||
kotlin("plugin.spring") version "1.9.22"
|
||||
kotlin("plugin.jpa") version "1.9.22"
|
||||
id("io.freefair.lombok") version "8.4"
|
||||
java
|
||||
}
|
||||
|
||||
@@ -17,10 +16,9 @@ allOpen {
|
||||
|
||||
group = "de.grimsi"
|
||||
version = "2.0.0-SNAPSHOT"
|
||||
description = "gameyfin"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
java.sourceCompatibility = JavaVersion.VERSION_21
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
@@ -29,20 +27,26 @@ configurations {
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
name = "Vaadin Addons"
|
||||
url = uri("https://maven.vaadin.com/vaadin-addons")
|
||||
setUrl("https://maven.vaadin.com/vaadin-addons")
|
||||
}
|
||||
}
|
||||
|
||||
extra["vaadinVersion"] = "24.3.3"
|
||||
extra["hillaVersion"] = "2.5.6"
|
||||
|
||||
dependencies {
|
||||
// Sprint Boot
|
||||
// Spring Boot & Kotlin
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("jakarta.validation:jakarta.validation-api:3.0.2")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
|
||||
// Hilla
|
||||
api("dev.hilla:hilla-react")
|
||||
api("dev.hilla:hilla-spring-boot-starter")
|
||||
|
||||
// Logging
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:6.0.3")
|
||||
@@ -51,35 +55,18 @@ dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.7")
|
||||
|
||||
// Frontend
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("com.vaadin:vaadin-spring-boot-starter")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("com.github.mvysny.karibudsl:karibu-dsl-v23:2.1.0")
|
||||
implementation("com.github.mvysny.karibu-tools:karibu-tools-23:0.19")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
|
||||
// Vaadin Add-Ons
|
||||
implementation("com.flowingcode.addons:font-awesome-iron-iconset:5.2.2")
|
||||
implementation("in.virit:viritin:2.7.0")
|
||||
implementation("io.sunshower.aire:aire-wizard:1.0.17.Final")
|
||||
|
||||
// Development
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
runtimeOnly("com.h2database:h2")
|
||||
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("org.springframework.security:spring-security-test")
|
||||
|
||||
// Fix compilation error
|
||||
compileOnly("com.github.spotbugs:spotbugs-annotations:4.8.3")
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom("com.vaadin:vaadin-bom:${property("vaadinVersion")}")
|
||||
mavenBom("dev.hilla:hilla-bom:${property("hillaVersion")}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import router from 'Frontend/routes.js';
|
||||
import {AuthProvider} from 'Frontend/util/auth.js';
|
||||
import {RouterProvider} from 'react-router-dom';
|
||||
import "./main.css";
|
||||
import {ThemeProvider} from "@material-tailwind/react";
|
||||
import React from 'react';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router}/>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
+19
-17
@@ -1,23 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This file is auto-generated by Vaadin.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body, #outlet {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Gameyfin</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#outlet {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- This outlet div is where the views are rendered -->
|
||||
<div id="outlet"></div>
|
||||
<div id="outlet"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import {createElement} from "react";
|
||||
import App from "Frontend/App";
|
||||
|
||||
createRoot(document.getElementById('outlet')!).render(createElement(App));
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,21 @@
|
||||
import {protectRoutes} from '@hilla/react-auth';
|
||||
import {createBrowserRouter, RouteObject} from 'react-router-dom';
|
||||
import LoginView from "Frontend/views/LoginView";
|
||||
import MainLayout from "Frontend/views/MainLayout";
|
||||
import TestView from "Frontend/views/TestView";
|
||||
|
||||
export const routes = protectRoutes([
|
||||
{
|
||||
element: <MainLayout/>,
|
||||
handle: {title: 'Main', requiresLogin: true},
|
||||
children: [
|
||||
{path: '/', element: <TestView/>, handle: {title: 'Gameyfin', requiresLogin: true}},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <LoginView/>
|
||||
}
|
||||
]) as RouteObject[];
|
||||
|
||||
export default createBrowserRouter(routes);
|
||||
@@ -1,12 +0,0 @@
|
||||
window.applyTheme = () => {
|
||||
const theme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "";
|
||||
document.documentElement.setAttribute("theme", theme);
|
||||
};
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener('change', function () {
|
||||
window.applyTheme()
|
||||
});
|
||||
window.applyTheme();
|
||||
@@ -1,6 +0,0 @@
|
||||
.header-logo {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
align-self: center;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {configureAuth} from '@hilla/react-auth';
|
||||
import {UserEndpoint} from 'Frontend/generated/endpoints';
|
||||
|
||||
// Configure auth to use `UserInfoService.getUserInfo`
|
||||
const auth = configureAuth(UserEndpoint.getUserInfo);
|
||||
|
||||
// Export auth provider and useAuth hook, which are automatically
|
||||
// typed to the result of `UserInfoService.getUserInfo`
|
||||
export const useAuth = auth.useAuth;
|
||||
export const AuthProvider = auth.AuthProvider;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useMatches } from 'react-router-dom';
|
||||
|
||||
type RouteMetadata = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the `handle` object containing the metadata for the current route,
|
||||
* or undefined if the route does not have defined a handle.
|
||||
*/
|
||||
export function useRouteMetadata(): RouteMetadata | undefined {
|
||||
const matches = useMatches();
|
||||
const match = matches[matches.length - 1];
|
||||
return match?.handle as RouteMetadata | undefined;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
import {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {Button, Card, Input, Typography} from "@material-tailwind/react";
|
||||
|
||||
export default function LoginView() {
|
||||
const {state, login} = useAuth();
|
||||
const [hasError, setError] = useState<boolean>();
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [password, setPassword] = useState<string>();
|
||||
const [url, setUrl] = useState<string>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (state.user && url) {
|
||||
const path = new URL(url, document.baseURI).pathname;
|
||||
navigate(path, {replace: true});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div className="fixed h-full w-full bg-gradient-to-br from-gf-primary to-gf-secondary"></div>
|
||||
<Card className="m-auto p-12" shadow={true}>
|
||||
<img
|
||||
className="h-28 w-full content-center"
|
||||
src="/images/Logo.svg"
|
||||
/>
|
||||
<div className="mt-8 mb-2 w-80 max-w-screen-lg sm:w-96">
|
||||
<form
|
||||
className="mb-1 flex flex-col gap-6"
|
||||
onSubmit={async e => {
|
||||
e.preventDefault();
|
||||
if (typeof username === "string" && password != null) {
|
||||
const {defaultUrl, error, redirectUrl} = await login(username, password);
|
||||
if (error) {
|
||||
setError(true);
|
||||
alert("Wrong username and/or password!");
|
||||
} else {
|
||||
setUrl(redirectUrl ?? defaultUrl ?? '/');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="blue-gray" className="-mb-3">
|
||||
Username
|
||||
</Typography>
|
||||
<Input
|
||||
onChange={(event) => {
|
||||
setUsername(event.target.value);
|
||||
}}
|
||||
size="lg"
|
||||
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||
labelProps={{
|
||||
className: "before:content-none after:content-none",
|
||||
}}
|
||||
crossOrigin="" //TODO: see https://github.com/creativetimofficial/material-tailwind/issues/427
|
||||
/>
|
||||
<Typography variant="h6" color="blue-gray" className="-mb-3">
|
||||
Password
|
||||
</Typography>
|
||||
<Input
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
type="password"
|
||||
size="lg"
|
||||
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||
labelProps={{
|
||||
className: "before:content-none after:content-none",
|
||||
}}
|
||||
crossOrigin="" //TODO: see https://github.com/creativetimofficial/material-tailwind/issues/427
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
fullWidth>
|
||||
Log in
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {AppLayout} from '@hilla/react-components/AppLayout.js';
|
||||
import {Avatar} from '@hilla/react-components/Avatar.js';
|
||||
import {Button} from '@hilla/react-components/Button.js';
|
||||
import {DrawerToggle} from '@hilla/react-components/DrawerToggle.js';
|
||||
import {useAuth} from 'Frontend/util/auth.js';
|
||||
import {useRouteMetadata} from 'Frontend/util/routing.js';
|
||||
import {useEffect} from 'react';
|
||||
import {Outlet} from 'react-router-dom';
|
||||
|
||||
const navLinkClasses = ({isActive}: any) => {
|
||||
return `block rounded-m p-s ${isActive ? 'bg-primary-10 text-primary' : 'text-body'}`;
|
||||
};
|
||||
|
||||
export default function MainLayout() {
|
||||
const currentTitle = useRouteMetadata()?.title ?? 'My App';
|
||||
useEffect(() => {
|
||||
document.title = currentTitle;
|
||||
}, [currentTitle]);
|
||||
|
||||
const {state, logout} = useAuth();
|
||||
return (
|
||||
<AppLayout primarySection="drawer">
|
||||
<div slot="drawer" className="flex flex-col justify-between h-full p-m">
|
||||
<header className="flex flex-col gap-m">
|
||||
<h1 className="text-l m-0">My App</h1>
|
||||
<nav>
|
||||
</nav>
|
||||
</header>
|
||||
<footer className="flex flex-col gap-s">
|
||||
{state.user ? (
|
||||
<>
|
||||
<div className="flex items-center gap-s">
|
||||
<Avatar theme="xsmall" name={state.user.name}/>
|
||||
{state.user.name}
|
||||
</div>
|
||||
<Button onClick={async () => logout()}>Sign out</Button>
|
||||
</>
|
||||
) : (
|
||||
<a href="/login">Sign in</a>
|
||||
)}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<DrawerToggle slot="navbar" aria-label="Menu toggle"></DrawerToggle>
|
||||
<h2 slot="navbar" className="text-l m-0">
|
||||
{currentTitle}
|
||||
</h2>
|
||||
|
||||
<Outlet/>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default function TestView() {
|
||||
return (
|
||||
<h1>Hello Gameyfin!</h1>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# This file was generated by the Gradle 'init' task.
|
||||
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||
|
||||
[versions]
|
||||
com-h2database-h2 = "2.2.224"
|
||||
dev-hilla-hilla-react = "2.5.6"
|
||||
dev-hilla-hilla-spring-boot-starter = "2.5.6"
|
||||
org-parttio-line-awesome = "2.0.0"
|
||||
org-springframework-boot-spring-boot-devtools = "3.2.2"
|
||||
org-springframework-boot-spring-boot-starter-data-jpa = "3.2.2"
|
||||
org-springframework-boot-spring-boot-starter-oauth2-resource-server = "3.2.2"
|
||||
org-springframework-boot-spring-boot-starter-security = "3.2.2"
|
||||
org-springframework-boot-spring-boot-starter-test = "3.2.2"
|
||||
org-springframework-boot-spring-boot-starter-validation = "3.2.2"
|
||||
|
||||
[libraries]
|
||||
com-h2database-h2 = { module = "com.h2database:h2", version.ref = "com-h2database-h2" }
|
||||
dev-hilla-hilla-react = { module = "dev.hilla:hilla-react", version.ref = "dev-hilla-hilla-react" }
|
||||
dev-hilla-hilla-spring-boot-starter = { module = "dev.hilla:hilla-spring-boot-starter", version.ref = "dev-hilla-hilla-spring-boot-starter" }
|
||||
org-parttio-line-awesome = { module = "org.parttio:line-awesome", version.ref = "org-parttio-line-awesome" }
|
||||
org-springframework-boot-spring-boot-devtools = { module = "org.springframework.boot:spring-boot-devtools", version.ref = "org-springframework-boot-spring-boot-devtools" }
|
||||
org-springframework-boot-spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "org-springframework-boot-spring-boot-starter-data-jpa" }
|
||||
org-springframework-boot-spring-boot-starter-oauth2-resource-server = { module = "org.springframework.boot:spring-boot-starter-oauth2-resource-server", version.ref = "org-springframework-boot-spring-boot-starter-oauth2-resource-server" }
|
||||
org-springframework-boot-spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "org-springframework-boot-spring-boot-starter-security" }
|
||||
org-springframework-boot-spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "org-springframework-boot-spring-boot-starter-test" }
|
||||
org-springframework-boot-spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation", version.ref = "org-springframework-boot-spring-boot-starter-validation" }
|
||||
Generated
+11301
File diff suppressed because it is too large
Load Diff
+135
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"name": "no-name",
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@hilla/form": "2.5.6",
|
||||
"@hilla/frontend": "2.5.6",
|
||||
"@hilla/generator-typescript-cli": "2.5.6",
|
||||
"@hilla/generator-typescript-core": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-backbone": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-barrel": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-client": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-model": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-push": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-subtypes": "2.5.6",
|
||||
"@hilla/generator-typescript-utils": "2.5.6",
|
||||
"@hilla/react-auth": "2.5.6",
|
||||
"@hilla/react-components": "2.3.0",
|
||||
"@hilla/react-crud": "2.5.6",
|
||||
"@hilla/react-form": "2.5.6",
|
||||
"@material-tailwind/react": "^2.1.9",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@vaadin/bundles": "24.3.0",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/router": "1.7.5",
|
||||
"classnames": "^2.3.2",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
"lit": "3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lit-labs/react": "^1.1.0",
|
||||
"@rollup/plugin-replace": "5.0.4",
|
||||
"@rollup/pluginutils": "5.0.5",
|
||||
"@types/react": "18.2.42",
|
||||
"@types/react-dom": "^18.0.8",
|
||||
"@vitejs/plugin-react": "4.2.0",
|
||||
"@vitejs/plugin-react-swc": "3.5.0",
|
||||
"async": "3.2.4",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"glob": "10.3.3",
|
||||
"postcss": "^8.4.35",
|
||||
"rollup-plugin-brotli": "3.1.0",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"strip-css-comments": "5.0.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"transform-ast": "2.4.4",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.6",
|
||||
"vite-plugin-checker": "0.6.2",
|
||||
"workbox-build": "7.0.0",
|
||||
"workbox-core": "7.0.0",
|
||||
"workbox-precaching": "7.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"classnames": "$classnames",
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom",
|
||||
"react-router-dom": "$react-router-dom",
|
||||
"@vaadin/bundles": "$@vaadin/bundles",
|
||||
"@hilla/react-components": "$@hilla/react-components",
|
||||
"@vaadin/common-frontend": "$@vaadin/common-frontend",
|
||||
"construct-style-sheets-polyfill": "$construct-style-sheets-polyfill",
|
||||
"lit": "$lit",
|
||||
"@vaadin/router": "$@vaadin/router",
|
||||
"@polymer/polymer": "$@polymer/polymer",
|
||||
"@hilla/react-auth": "$@hilla/react-auth",
|
||||
"@hilla/generator-typescript-plugin-model": "$@hilla/generator-typescript-plugin-model",
|
||||
"@hilla/generator-typescript-plugin-barrel": "$@hilla/generator-typescript-plugin-barrel",
|
||||
"@hilla/react-form": "$@hilla/react-form",
|
||||
"@hilla/generator-typescript-plugin-push": "$@hilla/generator-typescript-plugin-push",
|
||||
"@hilla/generator-typescript-core": "$@hilla/generator-typescript-core",
|
||||
"@hilla/generator-typescript-plugin-client": "$@hilla/generator-typescript-plugin-client",
|
||||
"@hilla/generator-typescript-utils": "$@hilla/generator-typescript-utils",
|
||||
"@hilla/frontend": "$@hilla/frontend",
|
||||
"@hilla/form": "$@hilla/form",
|
||||
"@hilla/generator-typescript-plugin-subtypes": "$@hilla/generator-typescript-plugin-subtypes",
|
||||
"@hilla/react-crud": "$@hilla/react-crud",
|
||||
"@hilla/generator-typescript-plugin-backbone": "$@hilla/generator-typescript-plugin-backbone",
|
||||
"@hilla/generator-typescript-cli": "$@hilla/generator-typescript-cli",
|
||||
"@material-tailwind/react": "$@material-tailwind/react",
|
||||
"@fortawesome/fontawesome-svg-core": "$@fortawesome/fontawesome-svg-core",
|
||||
"@fortawesome/free-solid-svg-icons": "$@fortawesome/free-solid-svg-icons",
|
||||
"@fortawesome/react-fontawesome": "$@fortawesome/react-fontawesome"
|
||||
},
|
||||
"vaadin": {
|
||||
"dependencies": {
|
||||
"@hilla/form": "2.5.6",
|
||||
"@hilla/frontend": "2.5.6",
|
||||
"@hilla/generator-typescript-cli": "2.5.6",
|
||||
"@hilla/generator-typescript-core": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-backbone": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-barrel": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-client": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-model": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-push": "2.5.6",
|
||||
"@hilla/generator-typescript-plugin-subtypes": "2.5.6",
|
||||
"@hilla/generator-typescript-utils": "2.5.6",
|
||||
"@hilla/react-auth": "2.5.6",
|
||||
"@hilla/react-components": "2.3.0",
|
||||
"@hilla/react-crud": "2.5.6",
|
||||
"@hilla/react-form": "2.5.6",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@vaadin/bundles": "24.3.0",
|
||||
"@vaadin/common-frontend": "0.0.19",
|
||||
"@vaadin/router": "1.7.5",
|
||||
"construct-style-sheets-polyfill": "3.1.0",
|
||||
"lit": "3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-replace": "5.0.4",
|
||||
"@rollup/pluginutils": "5.0.5",
|
||||
"@vitejs/plugin-react": "4.2.0",
|
||||
"@vitejs/plugin-react-swc": "3.5.0",
|
||||
"async": "3.2.4",
|
||||
"glob": "10.3.3",
|
||||
"rollup-plugin-brotli": "3.1.0",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"strip-css-comments": "5.0.0",
|
||||
"transform-ast": "2.4.4",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.6",
|
||||
"vite-plugin-checker": "0.6.2",
|
||||
"workbox-build": "7.0.0",
|
||||
"workbox-core": "7.0.0",
|
||||
"workbox-precaching": "7.0.0"
|
||||
},
|
||||
"hash": "f9e182b004ea4d86a6ca514e8d956df5851830853b0a45b9e40615d51b7d2788"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
projectJDK: "21" #(Applied in CI/CD pipeline)
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-jvm:latest
|
||||
@@ -1,32 +0,0 @@
|
||||
This directory is automatically generated by Vaadin and contains the pre-compiled
|
||||
frontend files/resources for your project (frontend development bundle).
|
||||
|
||||
It should be added to Version Control System and committed, so that other developers
|
||||
do not have to compile it again.
|
||||
|
||||
Frontend development bundle is automatically updated when needed:
|
||||
- an npm/pnpm package is added with @NpmPackage or directly into package.json
|
||||
- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript
|
||||
- Vaadin add-on with front-end customizations is added
|
||||
- Custom theme imports/assets added into 'theme.json' file
|
||||
- Exported web component is added.
|
||||
|
||||
If your project development needs a hot deployment of the frontend changes,
|
||||
you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions):
|
||||
- set `vaadin.frontend.hotdeploy=true` in `application.properties`
|
||||
- configure `vaadin-maven-plugin`:
|
||||
```
|
||||
<configuration>
|
||||
<frontendHotdeploy>true</frontendHotdeploy>
|
||||
</configuration>
|
||||
```
|
||||
- configure `jetty-maven-plugin`:
|
||||
```
|
||||
<configuration>
|
||||
<systemProperties>
|
||||
<vaadin.frontend.hotdeploy>true</vaadin.frontend.hotdeploy>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
Read more [about Vaadin development mode](https://vaadin.com/docs/next/configuration/development-mode/#pre-compiled-front-end-bundle-for-faster-start-up).
|
||||
Binary file not shown.
@@ -1,90 +0,0 @@
|
||||
package de.grimsi.gameyfin.layouts;
|
||||
|
||||
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome;
|
||||
import com.vaadin.flow.component.ClickEvent;
|
||||
import com.vaadin.flow.component.ComponentEventListener;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.applayout.AppLayout;
|
||||
import com.vaadin.flow.component.avatar.Avatar;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.contextmenu.MenuItem;
|
||||
import com.vaadin.flow.component.contextmenu.SubMenu;
|
||||
import com.vaadin.flow.component.dependency.CssImport;
|
||||
import com.vaadin.flow.component.dependency.JsModule;
|
||||
import com.vaadin.flow.component.html.Image;
|
||||
import com.vaadin.flow.component.menubar.MenuBar;
|
||||
import com.vaadin.flow.component.menubar.MenuBarVariant;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.spring.security.AuthenticationContext;
|
||||
import de.grimsi.gameyfin.resources.PublicResources;
|
||||
import de.grimsi.gameyfin.services.ThemeService;
|
||||
import de.grimsi.gameyfin.setup.SetupService;
|
||||
import de.grimsi.gameyfin.views.SetupView;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.vaadin.firitin.util.style.LumoProps;
|
||||
|
||||
import static de.grimsi.gameyfin.users.util.Utils.isAdmin;
|
||||
|
||||
@JsModule("./scripts/prefers-color-scheme.js")
|
||||
@CssImport("./styles/header.css")
|
||||
public class MainLayout extends AppLayout {
|
||||
|
||||
public MainLayout(AuthenticationContext authContext,
|
||||
@Autowired SetupService setupService,
|
||||
@Autowired ThemeService themeService) {
|
||||
|
||||
if (!setupService.isSetupCompleted()) {
|
||||
UI.getCurrent().navigate(SetupView.class);
|
||||
UI.getCurrent().close();
|
||||
}
|
||||
|
||||
UserDetails user = authContext.getAuthenticatedUser(UserDetails.class).get();
|
||||
|
||||
Image logo = new Image(PublicResources.GAMEYFIN_LOGO.path, "Gameyfin Logo");
|
||||
logo.addClassName("header-logo");
|
||||
|
||||
Button toggleTheme = new Button(FontAwesome.Solid.CIRCLE_HALF_STROKE.create());
|
||||
toggleTheme.addThemeVariants(ButtonVariant.LUMO_ICON);
|
||||
toggleTheme.addClickListener(listener -> themeService.toggleTheme());
|
||||
|
||||
Avatar avatar = new Avatar(user.getUsername());
|
||||
avatar.setAbbreviation(user.getUsername().substring(0, 2).toUpperCase());
|
||||
avatar.setColorIndex(user.getUsername().chars().map(i -> i % 6).findFirst().getAsInt());
|
||||
|
||||
MenuBar menu = new MenuBar();
|
||||
menu.addThemeVariants(MenuBarVariant.LUMO_ICON);
|
||||
MenuItem item = menu.addItem(avatar);
|
||||
SubMenu subMenu = item.getSubMenu();
|
||||
subMenu.addItem(menuItem(FontAwesome.Solid.USER, "Profile", l -> Notification.show("Profile")));
|
||||
if (isAdmin(user)) {
|
||||
subMenu.addItem(menuItem(FontAwesome.Solid.COG, "Administration", l -> Notification.show("Administration")));
|
||||
}
|
||||
subMenu.addItem(menuItem(FontAwesome.Solid.QUESTION_CIRCLE, "Help", l -> Notification.show("Help")));
|
||||
subMenu.addItem(menuItem(FontAwesome.Solid.SIGN_OUT, "Sign out", l -> authContext.logout()));
|
||||
|
||||
HorizontalLayout horizontalLayout = new HorizontalLayout();
|
||||
horizontalLayout.setAlignItems(FlexComponent.Alignment.END);
|
||||
horizontalLayout.add(logo, toggleTheme, menu);
|
||||
|
||||
addToNavbar(horizontalLayout);
|
||||
}
|
||||
|
||||
private HorizontalLayout menuItem(FontAwesome.Solid icon, String title, ComponentEventListener<ClickEvent<HorizontalLayout>> listener) {
|
||||
FontAwesome.Solid.Icon i = icon.create();
|
||||
i.setSize(LumoProps.ICON_SIZE_S.var());
|
||||
|
||||
HorizontalLayout h = new HorizontalLayout();
|
||||
h.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
h.setJustifyContentMode(FlexComponent.JustifyContentMode.START);
|
||||
|
||||
h.add(i);
|
||||
h.add(title);
|
||||
h.addClickListener(listener);
|
||||
|
||||
return h;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package de.grimsi.gameyfin.layouts;
|
||||
|
||||
import com.flowingcode.vaadin.addons.fontawesome.FontAwesome;
|
||||
import com.vaadin.flow.component.applayout.AppLayout;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.dependency.CssImport;
|
||||
import com.vaadin.flow.component.html.Image;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import de.grimsi.gameyfin.resources.PublicResources;
|
||||
import de.grimsi.gameyfin.services.ThemeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
@CssImport("./styles/header.css")
|
||||
public class SetupLayout extends AppLayout {
|
||||
|
||||
public SetupLayout(@Autowired ThemeService themeService) {
|
||||
Image logo = new Image(PublicResources.GAMEYFIN_LOGO.path, "Gameyfin Logo");
|
||||
logo.addClassName("header-logo");
|
||||
|
||||
Button toggleTheme = new Button(FontAwesome.Solid.CIRCLE_HALF_STROKE.create());
|
||||
toggleTheme.addThemeVariants(ButtonVariant.LUMO_ICON);
|
||||
toggleTheme.addClickListener(listener -> themeService.toggleTheme());
|
||||
|
||||
HorizontalLayout horizontalLayout = new HorizontalLayout();
|
||||
horizontalLayout.setWidthFull();
|
||||
horizontalLayout.setAlignSelf(FlexComponent.Alignment.END);
|
||||
horizontalLayout.add(logo, toggleTheme);
|
||||
|
||||
addToNavbar(horizontalLayout);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package de.grimsi.gameyfin.resources;
|
||||
|
||||
public enum PublicResources {
|
||||
GAMEYFIN_LOGO("public/images/Logo.svg");
|
||||
|
||||
public final String path;
|
||||
|
||||
PublicResources(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package de.grimsi.gameyfin.services;
|
||||
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ThemeService {
|
||||
public void toggleTheme() {
|
||||
Notification.show("Not implemented");
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package de.grimsi.gameyfin.views;
|
||||
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.html.Image;
|
||||
import com.vaadin.flow.component.login.LoginForm;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.BeforeEnterEvent;
|
||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||
import de.grimsi.gameyfin.resources.PublicResources;
|
||||
import de.grimsi.gameyfin.setup.SetupService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
|
||||
@Route("login")
|
||||
@PageTitle("Login")
|
||||
@AnonymousAllowed
|
||||
public class LoginView extends VerticalLayout implements BeforeEnterObserver {
|
||||
|
||||
private final LoginForm login = new LoginForm();
|
||||
|
||||
public LoginView(@Autowired SetupService setupService) {
|
||||
if (!setupService.isSetupCompleted()) {
|
||||
UI.getCurrent().navigate(SetupView.class);
|
||||
UI.getCurrent().close();
|
||||
}
|
||||
Image logo = new Image(PublicResources.GAMEYFIN_LOGO.path, "Gameyfin");
|
||||
logo.setHeight("100px");
|
||||
|
||||
login.setAction("login");
|
||||
|
||||
add(logo);
|
||||
add(login);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
|
||||
if (beforeEnterEvent.getLocation()
|
||||
.getQueryParameters()
|
||||
.getParameters()
|
||||
.containsKey("error")
|
||||
) {
|
||||
login.setError(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package de.grimsi.gameyfin.views;
|
||||
|
||||
import com.vaadin.flow.component.html.H1;
|
||||
import com.vaadin.flow.component.html.Pre;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.grimsi.gameyfin.layouts.MainLayout;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
|
||||
|
||||
@Route(value = "", layout = MainLayout.class)
|
||||
@PageTitle("Gameyfin")
|
||||
@PermitAll
|
||||
public class MainView extends VerticalLayout {
|
||||
|
||||
public MainView() {
|
||||
add(new H1("Gameyfin main page"));
|
||||
add(new Pre("Work in progress"));
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package de.grimsi.gameyfin.views;
|
||||
|
||||
import com.vaadin.flow.component.Text;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.PasswordField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||
import de.grimsi.gameyfin.layouts.SetupLayout;
|
||||
import de.grimsi.gameyfin.setup.SetupService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
|
||||
@Route(value = "/setup", layout = SetupLayout.class)
|
||||
@PageTitle("Setup")
|
||||
@AnonymousAllowed
|
||||
public class SetupView extends VerticalLayout {
|
||||
|
||||
public SetupView(@Autowired SetupService setupService) {
|
||||
if (setupService.isSetupCompleted()) {
|
||||
UI.getCurrent().navigate(LoginView.class);
|
||||
UI.getCurrent().close();
|
||||
}
|
||||
|
||||
setWidthFull();
|
||||
setAlignItems(Alignment.CENTER);
|
||||
|
||||
add(new Text("Looks like it's your first time starting Gameyfin. Let's continue setting up your very own instance 🙂"));
|
||||
|
||||
TextField username = new TextField("Username");
|
||||
username.focus();
|
||||
PasswordField passwordField = new PasswordField("Password");
|
||||
PasswordField passwordFieldRepeat = new PasswordField("Password (repeated)");
|
||||
|
||||
FormLayout form = new FormLayout();
|
||||
form.add(new Text("Let's start with creating a super admin account. This account will have full permissions."));
|
||||
form.add(username, passwordField, passwordFieldRepeat);
|
||||
|
||||
add(form);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-8
@@ -1,7 +1,6 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import com.vaadin.flow.spring.security.VaadinWebSecurity
|
||||
import de.grimsi.gameyfin.views.LoginView
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
@@ -15,7 +14,8 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
class SecurityConfiguration : VaadinWebSecurity() {
|
||||
class SecurityConfig : VaadinWebSecurity() {
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun configure(http: HttpSecurity) {
|
||||
// Configure your static resources with public access before calling super.configure(HttpSecurity) as it adds final anyRequest matcher
|
||||
@@ -23,18 +23,14 @@ class SecurityConfiguration : VaadinWebSecurity() {
|
||||
auth.requestMatchers(AntPathRequestMatcher("/public/**")).permitAll()
|
||||
}
|
||||
|
||||
// Configure your static resources with public access before calling
|
||||
// super.configure(HttpSecurity) as it adds final anyRequest matcher
|
||||
|
||||
super.configure(http)
|
||||
|
||||
// This is important to register your login view to the navigation access control mechanism:
|
||||
setLoginView(http, LoginView::class.java)
|
||||
setLoginView(http, "/login")
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
public override fun configure(web: WebSecurity) {
|
||||
super.configure(web)
|
||||
web.ignoring().requestMatchers(AntPathRequestMatcher("/images/**"))
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -26,17 +26,21 @@ class SetupDataLoader(
|
||||
log.info { "We will now set up some data..." }
|
||||
|
||||
setupRoles()
|
||||
//setupUser()
|
||||
setupUsers()
|
||||
|
||||
log.info { "Setup completed..." }
|
||||
}
|
||||
|
||||
fun setupUser() {
|
||||
fun setupUsers() {
|
||||
val superadmin = User("admin")
|
||||
superadmin.password = "admin"
|
||||
superadmin.roles = listOf(roleRepository.findByRolename(Roles.SUPERADMIN.roleName)!!)
|
||||
|
||||
userService.registerUser(superadmin)
|
||||
|
||||
val user = User("user")
|
||||
user.password = "user"
|
||||
user.roles = listOf(roleRepository.findByRolename(Roles.USER.roleName)!!)
|
||||
userService.registerUser(user)
|
||||
}
|
||||
|
||||
fun setupRoles() {
|
||||
|
||||
@@ -3,12 +3,14 @@ package de.grimsi.gameyfin.setup
|
||||
import jakarta.servlet.*
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Component
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
//@Order(1)
|
||||
//@Component
|
||||
@Order(1)
|
||||
@Component
|
||||
class SetupFilter(
|
||||
private val setupService: SetupService
|
||||
) : Filter {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.grimsi.gameyfin.users
|
||||
|
||||
import de.grimsi.gameyfin.users.dto.UserInfo
|
||||
import dev.hilla.Endpoint
|
||||
import jakarta.annotation.security.PermitAll
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
|
||||
@Endpoint
|
||||
class UserEndpoint {
|
||||
|
||||
@PermitAll
|
||||
fun getUserInfo(): UserInfo {
|
||||
val auth: Authentication = SecurityContextHolder.getContext().authentication
|
||||
val authorities: List<String> = auth.authorities.map { g: GrantedAuthority -> g.authority }
|
||||
return UserInfo(auth.name, authorities)
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class UserService(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
getAuthorities(user.roles)
|
||||
toAuthorities(user.roles)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class UserService(
|
||||
return userRepository.save(user)
|
||||
}
|
||||
|
||||
private fun getAuthorities(roles: Collection<Role>): List<GrantedAuthority> {
|
||||
private fun toAuthorities(roles: Collection<Role>): List<GrantedAuthority> {
|
||||
return roles.map { r -> SimpleGrantedAuthority(r.rolename) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.grimsi.gameyfin.users.dto
|
||||
|
||||
data class UserInfo(
|
||||
val name: String,
|
||||
val authorities: List<String>
|
||||
)
|
||||
@@ -6,24 +6,25 @@ import jakarta.validation.constraints.NotNull
|
||||
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
class User(
|
||||
@NotNull
|
||||
@field:NotNull
|
||||
var username: String,
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
var id: Long? = null,
|
||||
|
||||
@NotNull
|
||||
@field:NotNull
|
||||
var password: String? = null,
|
||||
|
||||
@Nullable
|
||||
@field:Nullable
|
||||
var email: String? = null,
|
||||
|
||||
var enabled: Boolean = true,
|
||||
|
||||
@Embedded
|
||||
@Nullable
|
||||
@field:Nullable
|
||||
var avatar: Avatar? = null,
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,14 +1,28 @@
|
||||
logging.level:
|
||||
org.atmosphere: warn
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
session:
|
||||
tracking-modes: cookie
|
||||
|
||||
spring:
|
||||
# Workaround for https://github.dev/hilla/issues/842
|
||||
devtools.restart.additional-exclude: dev/hilla/openapi.json
|
||||
jpa:
|
||||
defer-datasource-initialization: true
|
||||
mustache:
|
||||
check-template-location: false
|
||||
sql.init.mode: always
|
||||
application:
|
||||
name: Gameyfin
|
||||
|
||||
vaadin:
|
||||
launch-browser: true
|
||||
# To improve the performance during development.
|
||||
# For more information https://vaadin.com/docs/flow/spring/tutorial-spring-configuration.html#special-configuration-parameters
|
||||
whitelisted-packages:
|
||||
- com.vaadin
|
||||
- org.vaadin
|
||||
- dev.hilla
|
||||
- com.flowingcode
|
||||
frontend:
|
||||
hotdeploy: false
|
||||
|
||||
spring:
|
||||
jpa:
|
||||
properties:
|
||||
hibernate:
|
||||
globally_quoted_identifiers: true
|
||||
- dev.hilla
|
||||
@@ -0,0 +1,9 @@
|
||||
${AnsiBackground.DEFAULT}
|
||||
${AnsiColor.BLUE} _____ ${AnsiColor.MAGENTA} ___ _
|
||||
${AnsiColor.BLUE} / ___/ ___ _ __ _ ___ __ __${AnsiColor.MAGENTA} / _/ (_) ___
|
||||
${AnsiColor.BLUE}/ (_ / / _ `/ / ' \/ -_) / // /${AnsiColor.MAGENTA} / _/ / / / _ \
|
||||
${AnsiColor.BLUE}\___/ \_,_/ /_/_/_/\__/ \_, / ${AnsiColor.MAGENTA}/_/ /_/ /_//_/
|
||||
${AnsiColor.BLUE} /___/
|
||||
${AnsiColor.DEFAULT}
|
||||
${spring.application.name} ${application.version}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package de.grimsi.gameyfin
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class GameyfinApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import withMT from "@material-tailwind/react/utils/withMT";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default withMT({
|
||||
content: ["./frontend/index.html", "./frontend/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'gf-primary': '#2332c8',
|
||||
'gf-secondary': '#6441a5'
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
// This TypeScript configuration file is generated by vaadin-maven-plugin.
|
||||
// This is needed for TypeScript compiler to compile your TypeScript code in the project.
|
||||
// It is recommended to commit this file to the VCS.
|
||||
// You might want to change the configurations to fit your preferences
|
||||
// For more information about the configurations, please refer to http://www.typescriptlang.org/docs/handbook/tsconfig-json.html
|
||||
{
|
||||
"_version": "9",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"inlineSources": true,
|
||||
"module": "esNext",
|
||||
"target": "es2020",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"baseUrl": "frontend",
|
||||
"paths": {
|
||||
"@vaadin/flow-frontend": ["generated/jar-resources"],
|
||||
"@vaadin/flow-frontend/*": ["generated/jar-resources/*"],
|
||||
"Frontend/*": ["*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"frontend/**/*",
|
||||
"types.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"frontend/generated/jar-resources/**"
|
||||
]
|
||||
}
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
declare module '*.module.css' {
|
||||
declare const styles: Record<string, string>;
|
||||
export default styles;
|
||||
}
|
||||
declare module '*.module.sass' {
|
||||
declare const styles: Record<string, string>;
|
||||
export default styles;
|
||||
}
|
||||
declare module '*.module.scss' {
|
||||
declare const styles: Record<string, string>;
|
||||
export default styles;
|
||||
}
|
||||
declare module '*.module.less' {
|
||||
declare const classes: Record<string, string>;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.styl' {
|
||||
declare const classes: Record<string, string>;
|
||||
export default classes;
|
||||
}
|
||||
|
||||
/* CSS FILES */
|
||||
declare module '*.css';
|
||||
declare module '*.sass';
|
||||
declare module '*.scss';
|
||||
declare module '*.less';
|
||||
declare module '*.styl';
|
||||
|
||||
/* IMAGES */
|
||||
declare module '*.svg' {
|
||||
const ref: string;
|
||||
export default ref;
|
||||
}
|
||||
declare module '*.bmp' {
|
||||
const ref: string;
|
||||
export default ref;
|
||||
}
|
||||
declare module '*.gif' {
|
||||
const ref: string;
|
||||
export default ref;
|
||||
}
|
||||
declare module '*.jpg' {
|
||||
const ref: string;
|
||||
export default ref;
|
||||
}
|
||||
declare module '*.jpeg' {
|
||||
const ref: string;
|
||||
export default ref;
|
||||
}
|
||||
declare module '*.png' {
|
||||
const ref: string;
|
||||
export default ref;
|
||||
}
|
||||
declare module '*.avif' {
|
||||
const ref: string;
|
||||
export default ref;
|
||||
}
|
||||
declare module '*.webp' {
|
||||
const ref: string;
|
||||
export default ref;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import reactSwc from '@vitejs/plugin-react-swc';
|
||||
import type { UserConfigFn } from 'vite';
|
||||
import { overrideVaadinConfig } from './vite.generated';
|
||||
|
||||
const customConfig: UserConfigFn = (env) => ({
|
||||
// Here you can add custom Vite parameters
|
||||
// https://vitejs.dev/config/
|
||||
plugins: [
|
||||
reactSwc({
|
||||
tsDecorators: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export default overrideVaadinConfig(customConfig);
|
||||
@@ -0,0 +1,853 @@
|
||||
/**
|
||||
* NOTICE: this is an auto-generated file
|
||||
*
|
||||
* This file has been generated by the `flow:prepare-frontend` maven goal.
|
||||
* This file will be overwritten on every run. Any custom changes should be made to vite.config.ts
|
||||
*/
|
||||
import path from 'path';
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { createHash } from 'crypto';
|
||||
import * as net from 'net';
|
||||
|
||||
import { processThemeResources } from './build/plugins/application-theme-plugin/theme-handle.js';
|
||||
import { rewriteCssUrls } from './build/plugins/theme-loader/theme-loader-utils.js';
|
||||
import settings from './build/vaadin-dev-server-settings.json';
|
||||
import {
|
||||
AssetInfo,
|
||||
ChunkInfo,
|
||||
defineConfig,
|
||||
mergeConfig,
|
||||
OutputOptions,
|
||||
PluginOption,
|
||||
ResolvedConfig,
|
||||
UserConfigFn
|
||||
} from 'vite';
|
||||
import { getManifest } from 'workbox-build';
|
||||
|
||||
import * as rollup from 'rollup';
|
||||
import brotli from 'rollup-plugin-brotli';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import checker from 'vite-plugin-checker';
|
||||
import postcssLit from './build/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js';
|
||||
|
||||
import { createRequire } from 'module';
|
||||
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
// Make `require` compatible with ES modules
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const appShellUrl = '.';
|
||||
|
||||
const frontendFolder = path.resolve(__dirname, settings.frontendFolder);
|
||||
const themeFolder = path.resolve(frontendFolder, settings.themeFolder);
|
||||
const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput);
|
||||
const devBundleFolder = path.resolve(__dirname, settings.devBundleOutput);
|
||||
const devBundle = !!process.env.devBundle;
|
||||
const jarResourcesFolder = path.resolve(__dirname, settings.jarResourcesFolder);
|
||||
const themeResourceFolder = path.resolve(__dirname, settings.themeResourceFolder);
|
||||
const projectPackageJsonFile = path.resolve(__dirname, 'package.json');
|
||||
|
||||
const buildOutputFolder = devBundle ? devBundleFolder : frontendBundleFolder;
|
||||
const statsFolder = path.resolve(__dirname, devBundle ? settings.devBundleStatsOutput : settings.statsOutput);
|
||||
const statsFile = path.resolve(statsFolder, 'stats.json');
|
||||
const bundleSizeFile = path.resolve(statsFolder, 'bundle-size.html');
|
||||
const nodeModulesFolder = path.resolve(__dirname, 'node_modules');
|
||||
const webComponentTags = '';
|
||||
|
||||
const projectIndexHtml = path.resolve(frontendFolder, 'index.html');
|
||||
|
||||
const projectStaticAssetsFolders = [
|
||||
path.resolve(__dirname, 'src', 'main', 'resources', 'META-INF', 'resources'),
|
||||
path.resolve(__dirname, 'src', 'main', 'resources', 'static'),
|
||||
frontendFolder
|
||||
];
|
||||
|
||||
// Folders in the project which can contain application themes
|
||||
const themeProjectFolders = projectStaticAssetsFolders.map((folder) => path.resolve(folder, settings.themeFolder));
|
||||
|
||||
const themeOptions = {
|
||||
devMode: false,
|
||||
useDevBundle: devBundle,
|
||||
// The following matches folder 'frontend/generated/themes/'
|
||||
// (not 'frontend/themes') for theme in JAR that is copied there
|
||||
themeResourceFolder: path.resolve(themeResourceFolder, settings.themeFolder),
|
||||
themeProjectFolders: themeProjectFolders,
|
||||
projectStaticAssetsOutputFolder: devBundle
|
||||
? path.resolve(devBundleFolder, '../assets')
|
||||
: path.resolve(__dirname, settings.staticOutput),
|
||||
frontendGeneratedFolder: path.resolve(frontendFolder, settings.generatedFolder)
|
||||
};
|
||||
|
||||
const hasExportedWebComponents = existsSync(path.resolve(frontendFolder, 'web-component.html'));
|
||||
|
||||
// Block debug and trace logs.
|
||||
console.trace = () => {};
|
||||
console.debug = () => {};
|
||||
|
||||
function injectManifestToSWPlugin(): rollup.Plugin {
|
||||
const rewriteManifestIndexHtmlUrl = (manifest) => {
|
||||
const indexEntry = manifest.find((entry) => entry.url === 'index.html');
|
||||
if (indexEntry) {
|
||||
indexEntry.url = appShellUrl;
|
||||
}
|
||||
|
||||
return { manifest, warnings: [] };
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'vaadin:inject-manifest-to-sw',
|
||||
async transform(code, id) {
|
||||
if (/sw\.(ts|js)$/.test(id)) {
|
||||
const { manifestEntries } = await getManifest({
|
||||
globDirectory: buildOutputFolder,
|
||||
globPatterns: ['**/*'],
|
||||
globIgnores: ['**/*.br'],
|
||||
manifestTransforms: [rewriteManifestIndexHtmlUrl],
|
||||
maximumFileSizeToCacheInBytes: 100 * 1024 * 1024 // 100mb,
|
||||
});
|
||||
|
||||
return code.replace('self.__WB_MANIFEST', JSON.stringify(manifestEntries));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildSWPlugin(opts): PluginOption {
|
||||
let config: ResolvedConfig;
|
||||
const devMode = opts.devMode;
|
||||
|
||||
const swObj = {};
|
||||
|
||||
async function build(action: 'generate' | 'write', additionalPlugins: rollup.Plugin[] = []) {
|
||||
const includedPluginNames = [
|
||||
'vite:esbuild',
|
||||
'rollup-plugin-dynamic-import-variables',
|
||||
'vite:esbuild-transpile',
|
||||
'vite:terser'
|
||||
];
|
||||
const plugins: rollup.Plugin[] = config.plugins.filter((p) => {
|
||||
return includedPluginNames.includes(p.name);
|
||||
});
|
||||
const resolver = config.createResolver();
|
||||
const resolvePlugin: rollup.Plugin = {
|
||||
name: 'resolver',
|
||||
resolveId(source, importer, _options) {
|
||||
return resolver(source, importer);
|
||||
}
|
||||
};
|
||||
plugins.unshift(resolvePlugin); // Put resolve first
|
||||
plugins.push(
|
||||
replace({
|
||||
values: {
|
||||
'process.env.NODE_ENV': JSON.stringify(config.mode),
|
||||
...config.define
|
||||
},
|
||||
preventAssignment: true
|
||||
})
|
||||
);
|
||||
if (additionalPlugins) {
|
||||
plugins.push(...additionalPlugins);
|
||||
}
|
||||
const bundle = await rollup.rollup({
|
||||
input: path.resolve(settings.clientServiceWorkerSource),
|
||||
plugins
|
||||
});
|
||||
|
||||
try {
|
||||
return await bundle[action]({
|
||||
file: path.resolve(buildOutputFolder, 'sw.js'),
|
||||
format: 'es',
|
||||
exports: 'none',
|
||||
sourcemap: config.command === 'serve' || config.build.sourcemap,
|
||||
inlineDynamicImports: true
|
||||
});
|
||||
} finally {
|
||||
await bundle.close();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vaadin:build-sw',
|
||||
enforce: 'post',
|
||||
async configResolved(resolvedConfig) {
|
||||
config = resolvedConfig;
|
||||
},
|
||||
async buildStart() {
|
||||
if (devMode) {
|
||||
const { output } = await build('generate');
|
||||
swObj.code = output[0].code;
|
||||
swObj.map = output[0].map;
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
if (id.endsWith('sw.js')) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
async transform(_code, id) {
|
||||
if (id.endsWith('sw.js')) {
|
||||
return swObj;
|
||||
}
|
||||
},
|
||||
async closeBundle() {
|
||||
if (!devMode) {
|
||||
await build('write', [injectManifestToSWPlugin(), brotli()]);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function statsExtracterPlugin(): PluginOption {
|
||||
function collectThemeJsonsInFrontend(themeJsonContents: Record<string, string>, themeName: string) {
|
||||
const themeJson = path.resolve(frontendFolder, settings.themeFolder, themeName, 'theme.json');
|
||||
if (existsSync(themeJson)) {
|
||||
const themeJsonContent = readFileSync(themeJson, { encoding: 'utf-8' }).replace(/\r\n/g, '\n');
|
||||
themeJsonContents[themeName] = themeJsonContent;
|
||||
const themeJsonObject = JSON.parse(themeJsonContent);
|
||||
if (themeJsonObject.parent) {
|
||||
collectThemeJsonsInFrontend(themeJsonContents, themeJsonObject.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vaadin:stats',
|
||||
enforce: 'post',
|
||||
async writeBundle(options: OutputOptions, bundle: { [fileName: string]: AssetInfo | ChunkInfo }) {
|
||||
const modules = Object.values(bundle).flatMap((b) => (b.modules ? Object.keys(b.modules) : []));
|
||||
const nodeModulesFolders = modules
|
||||
.map((id) => id.replace(/\\/g, '/'))
|
||||
.filter((id) => id.startsWith(nodeModulesFolder.replace(/\\/g, '/')))
|
||||
.map((id) => id.substring(nodeModulesFolder.length + 1));
|
||||
const npmModules = nodeModulesFolders
|
||||
.map((id) => id.replace(/\\/g, '/'))
|
||||
.map((id) => {
|
||||
const parts = id.split('/');
|
||||
if (id.startsWith('@')) {
|
||||
return parts[0] + '/' + parts[1];
|
||||
} else {
|
||||
return parts[0];
|
||||
}
|
||||
})
|
||||
.sort()
|
||||
.filter((value, index, self) => self.indexOf(value) === index);
|
||||
const npmModuleAndVersion = Object.fromEntries(npmModules.map((module) => [module, getVersion(module)]));
|
||||
const cvdls = Object.fromEntries(
|
||||
npmModules
|
||||
.filter((module) => getCvdlName(module) != null)
|
||||
.map((module) => [module, { name: getCvdlName(module), version: getVersion(module) }])
|
||||
);
|
||||
|
||||
mkdirSync(path.dirname(statsFile), { recursive: true });
|
||||
const projectPackageJson = JSON.parse(readFileSync(projectPackageJsonFile, { encoding: 'utf-8' }));
|
||||
|
||||
const entryScripts = Object.values(bundle)
|
||||
.filter((bundle) => bundle.isEntry)
|
||||
.map((bundle) => bundle.fileName);
|
||||
|
||||
const generatedIndexHtml = path.resolve(buildOutputFolder, 'index.html');
|
||||
const customIndexData: string = readFileSync(projectIndexHtml, { encoding: 'utf-8' });
|
||||
const generatedIndexData: string = readFileSync(generatedIndexHtml, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
|
||||
const customIndexRows = new Set(customIndexData.split(/[\r\n]/).filter((row) => row.trim() !== ''));
|
||||
const generatedIndexRows = generatedIndexData.split(/[\r\n]/).filter((row) => row.trim() !== '');
|
||||
|
||||
const rowsGenerated: string[] = [];
|
||||
generatedIndexRows.forEach((row) => {
|
||||
if (!customIndexRows.has(row)) {
|
||||
rowsGenerated.push(row);
|
||||
}
|
||||
});
|
||||
|
||||
//After dev-bundle build add used Flow frontend imports JsModule/JavaScript/CssImport
|
||||
|
||||
const parseImports = (filename: string, result: Set<string>): void => {
|
||||
const content: string = readFileSync(filename, { encoding: 'utf-8' });
|
||||
const lines = content.split('\n');
|
||||
const staticImports = lines
|
||||
.filter((line) => line.startsWith('import '))
|
||||
.map((line) => line.substring(line.indexOf("'") + 1, line.lastIndexOf("'")))
|
||||
.map((line) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line));
|
||||
const dynamicImports = lines
|
||||
.filter((line) => line.includes('import('))
|
||||
.map((line) => line.replace(/.*import\(/, ''))
|
||||
.map((line) => line.split(/'/)[1])
|
||||
.map((line) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line));
|
||||
|
||||
staticImports.forEach((staticImport) => result.add(staticImport));
|
||||
|
||||
dynamicImports.map((dynamicImport) => {
|
||||
const importedFile = path.resolve(path.dirname(filename), dynamicImport);
|
||||
parseImports(importedFile, result);
|
||||
});
|
||||
};
|
||||
|
||||
const generatedImportsSet = new Set<string>();
|
||||
parseImports(
|
||||
path.resolve(themeOptions.frontendGeneratedFolder, 'flow', 'generated-flow-imports.js'),
|
||||
generatedImportsSet
|
||||
);
|
||||
const generatedImports = Array.from(generatedImportsSet).sort();
|
||||
|
||||
const frontendFiles: Record<string, string> = {};
|
||||
|
||||
const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map'];
|
||||
|
||||
const isThemeComponentsResource = (id: string) =>
|
||||
id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
|
||||
&& id.match(/.*\/jar-resources\/themes\/[^\/]+\/components\//);
|
||||
|
||||
const isGeneratedWebComponentResource = (id: string) =>
|
||||
id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
|
||||
&& id.match(/.*\/flow\/web-components\//);
|
||||
|
||||
const isFrontendResourceCollected = (id: string) =>
|
||||
!id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
|
||||
|| isThemeComponentsResource(id)
|
||||
|| isGeneratedWebComponentResource(id);
|
||||
|
||||
// collects project's frontend resources in frontend folder, excluding
|
||||
// 'generated' sub-folder, except for legacy shadow DOM stylesheets
|
||||
// packaged in `theme/components/` folder
|
||||
// and generated web component resources in `flow/web-components` folder.
|
||||
modules
|
||||
.map((id) => id.replace(/\\/g, '/'))
|
||||
.filter((id) => id.startsWith(frontendFolder.replace(/\\/g, '/')))
|
||||
.filter(isFrontendResourceCollected)
|
||||
.map((id) => id.substring(frontendFolder.length + 1))
|
||||
.map((line: string) => (line.includes('?') ? line.substring(0, line.lastIndexOf('?')) : line))
|
||||
.forEach((line: string) => {
|
||||
// \r\n from windows made files may be used so change to \n
|
||||
const filePath = path.resolve(frontendFolder, line);
|
||||
if (projectFileExtensions.includes(path.extname(filePath))) {
|
||||
const fileBuffer = readFileSync(filePath, { encoding: 'utf-8' }).replace(/\r\n/g, '\n');
|
||||
frontendFiles[line] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex');
|
||||
}
|
||||
});
|
||||
|
||||
// collects frontend resources from the JARs
|
||||
generatedImports
|
||||
.filter((line: string) => line.includes('generated/jar-resources'))
|
||||
.forEach((line: string) => {
|
||||
let filename = line.substring(line.indexOf('generated'));
|
||||
// \r\n from windows made files may be used ro remove to be only \n
|
||||
const fileBuffer = readFileSync(path.resolve(frontendFolder, filename), { encoding: 'utf-8' }).replace(
|
||||
/\r\n/g,
|
||||
'\n'
|
||||
);
|
||||
const hash = createHash('sha256').update(fileBuffer, 'utf8').digest('hex');
|
||||
|
||||
const fileKey = line.substring(line.indexOf('jar-resources/') + 14);
|
||||
frontendFiles[fileKey] = hash;
|
||||
});
|
||||
// collects and hash rest of the Frontend resources excluding files in /generated/ and /themes/
|
||||
// and files already in frontendFiles.
|
||||
let frontendFolderAlias = "Frontend";
|
||||
generatedImports
|
||||
.filter((line: string) => line.startsWith(frontendFolderAlias + '/'))
|
||||
.filter((line: string) => !line.startsWith(frontendFolderAlias + '/generated/'))
|
||||
.filter((line: string) => !line.startsWith(frontendFolderAlias + '/themes/'))
|
||||
.map((line) => line.substring(frontendFolderAlias.length + 1))
|
||||
.filter((line: string) => !frontendFiles[line])
|
||||
.forEach((line: string) => {
|
||||
const filePath = path.resolve(frontendFolder, line);
|
||||
if (projectFileExtensions.includes(path.extname(filePath)) && existsSync(filePath)) {
|
||||
const fileBuffer = readFileSync(filePath, { encoding: 'utf-8' }).replace(/\r\n/g, '\n');
|
||||
frontendFiles[line] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex');
|
||||
}
|
||||
});
|
||||
// If a index.ts exists hash it to be able to see if it changes.
|
||||
if (existsSync(path.resolve(frontendFolder, 'index.ts'))) {
|
||||
const fileBuffer = readFileSync(path.resolve(frontendFolder, 'index.ts'), { encoding: 'utf-8' }).replace(
|
||||
/\r\n/g,
|
||||
'\n'
|
||||
);
|
||||
frontendFiles[`index.ts`] = createHash('sha256').update(fileBuffer, 'utf8').digest('hex');
|
||||
}
|
||||
|
||||
const themeJsonContents: Record<string, string> = {};
|
||||
const themesFolder = path.resolve(jarResourcesFolder, 'themes');
|
||||
if (existsSync(themesFolder)) {
|
||||
readdirSync(themesFolder).forEach((themeFolder) => {
|
||||
const themeJson = path.resolve(themesFolder, themeFolder, 'theme.json');
|
||||
if (existsSync(themeJson)) {
|
||||
themeJsonContents[path.basename(themeFolder)] = readFileSync(themeJson, { encoding: 'utf-8' }).replace(
|
||||
/\r\n/g,
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
collectThemeJsonsInFrontend(themeJsonContents, settings.themeName);
|
||||
|
||||
let webComponents: string[] = [];
|
||||
if (webComponentTags) {
|
||||
webComponents = webComponentTags.split(';');
|
||||
}
|
||||
|
||||
const stats = {
|
||||
packageJsonDependencies: projectPackageJson.dependencies,
|
||||
npmModules: npmModuleAndVersion,
|
||||
bundleImports: generatedImports,
|
||||
frontendHashes: frontendFiles,
|
||||
themeJsonContents: themeJsonContents,
|
||||
entryScripts,
|
||||
webComponents,
|
||||
cvdlModules: cvdls,
|
||||
packageJsonHash: projectPackageJson?.vaadin?.hash,
|
||||
indexHtmlGenerated: rowsGenerated
|
||||
};
|
||||
writeFileSync(statsFile, JSON.stringify(stats, null, 1));
|
||||
}
|
||||
};
|
||||
}
|
||||
function vaadinBundlesPlugin(): PluginOption {
|
||||
type ExportInfo =
|
||||
| string
|
||||
| {
|
||||
namespace?: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
type ExposeInfo = {
|
||||
exports: ExportInfo[];
|
||||
};
|
||||
|
||||
type PackageInfo = {
|
||||
version: string;
|
||||
exposes: Record<string, ExposeInfo>;
|
||||
};
|
||||
|
||||
type BundleJson = {
|
||||
packages: Record<string, PackageInfo>;
|
||||
};
|
||||
|
||||
const disabledMessage = 'Vaadin component dependency bundles are disabled.';
|
||||
|
||||
const modulesDirectory = nodeModulesFolder.replace(/\\/g, '/');
|
||||
|
||||
let vaadinBundleJson: BundleJson;
|
||||
|
||||
function parseModuleId(id: string): { packageName: string; modulePath: string } {
|
||||
const [scope, scopedPackageName] = id.split('/', 3);
|
||||
const packageName = scope.startsWith('@') ? `${scope}/${scopedPackageName}` : scope;
|
||||
const modulePath = `.${id.substring(packageName.length)}`;
|
||||
return {
|
||||
packageName,
|
||||
modulePath
|
||||
};
|
||||
}
|
||||
|
||||
function getExports(id: string): string[] | undefined {
|
||||
const { packageName, modulePath } = parseModuleId(id);
|
||||
const packageInfo = vaadinBundleJson.packages[packageName];
|
||||
|
||||
if (!packageInfo) return;
|
||||
|
||||
const exposeInfo: ExposeInfo = packageInfo.exposes[modulePath];
|
||||
if (!exposeInfo) return;
|
||||
|
||||
const exportsSet = new Set<string>();
|
||||
for (const e of exposeInfo.exports) {
|
||||
if (typeof e === 'string') {
|
||||
exportsSet.add(e);
|
||||
} else {
|
||||
const { namespace, source } = e;
|
||||
if (namespace) {
|
||||
exportsSet.add(namespace);
|
||||
} else {
|
||||
const sourceExports = getExports(source);
|
||||
if (sourceExports) {
|
||||
sourceExports.forEach((e) => exportsSet.add(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(exportsSet);
|
||||
}
|
||||
|
||||
function getExportBinding(binding: string) {
|
||||
return binding === 'default' ? '_default as default' : binding;
|
||||
}
|
||||
|
||||
function getImportAssigment(binding: string) {
|
||||
return binding === 'default' ? 'default: _default' : binding;
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vaadin:bundles',
|
||||
enforce: 'pre',
|
||||
apply(config, { command }) {
|
||||
if (command !== 'serve') return false;
|
||||
|
||||
try {
|
||||
const vaadinBundleJsonPath = require.resolve('@vaadin/bundles/vaadin-bundle.json');
|
||||
vaadinBundleJson = JSON.parse(readFileSync(vaadinBundleJsonPath, { encoding: 'utf8' }));
|
||||
} catch (e: unknown) {
|
||||
if (typeof e === 'object' && (e as { code: string }).code === 'MODULE_NOT_FOUND') {
|
||||
vaadinBundleJson = { packages: {} };
|
||||
console.info(`@vaadin/bundles npm package is not found, ${disabledMessage}`);
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const versionMismatches: Array<{ name: string; bundledVersion: string; installedVersion: string }> = [];
|
||||
for (const [name, packageInfo] of Object.entries(vaadinBundleJson.packages)) {
|
||||
let installedVersion: string | undefined = undefined;
|
||||
try {
|
||||
const { version: bundledVersion } = packageInfo;
|
||||
const installedPackageJsonFile = path.resolve(modulesDirectory, name, 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(installedPackageJsonFile, { encoding: 'utf8' }));
|
||||
installedVersion = packageJson.version;
|
||||
if (installedVersion && installedVersion !== bundledVersion) {
|
||||
versionMismatches.push({
|
||||
name,
|
||||
bundledVersion,
|
||||
installedVersion
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore package not found
|
||||
}
|
||||
}
|
||||
if (versionMismatches.length) {
|
||||
console.info(`@vaadin/bundles has version mismatches with installed packages, ${disabledMessage}`);
|
||||
console.info(`Packages with version mismatches: ${JSON.stringify(versionMismatches, undefined, 2)}`);
|
||||
vaadinBundleJson = { packages: {} };
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async config(config) {
|
||||
return mergeConfig(
|
||||
{
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
// Vaadin bundle
|
||||
'@vaadin/bundles',
|
||||
...Object.keys(vaadinBundleJson.packages),
|
||||
'@vaadin/vaadin-material-styles'
|
||||
]
|
||||
}
|
||||
},
|
||||
config
|
||||
);
|
||||
},
|
||||
load(rawId) {
|
||||
const [path, params] = rawId.split('?');
|
||||
if (!path.startsWith(modulesDirectory)) return;
|
||||
|
||||
const id = path.substring(modulesDirectory.length + 1);
|
||||
const bindings = getExports(id);
|
||||
if (bindings === undefined) return;
|
||||
|
||||
const cacheSuffix = params ? `?${params}` : '';
|
||||
const bundlePath = `@vaadin/bundles/vaadin.js${cacheSuffix}`;
|
||||
|
||||
return `import { init as VaadinBundleInit, get as VaadinBundleGet } from '${bundlePath}';
|
||||
await VaadinBundleInit('default');
|
||||
const { ${bindings.map(getImportAssigment).join(', ')} } = (await VaadinBundleGet('./node_modules/${id}'))();
|
||||
export { ${bindings.map(getExportBinding).join(', ')} };`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function themePlugin(opts): PluginOption {
|
||||
const fullThemeOptions = { ...themeOptions, devMode: opts.devMode };
|
||||
return {
|
||||
name: 'vaadin:theme',
|
||||
config() {
|
||||
processThemeResources(fullThemeOptions, console);
|
||||
},
|
||||
configureServer(server) {
|
||||
function handleThemeFileCreateDelete(themeFile, stats) {
|
||||
if (themeFile.startsWith(themeFolder)) {
|
||||
const changed = path.relative(themeFolder, themeFile);
|
||||
console.debug('Theme file ' + (!!stats ? 'created' : 'deleted'), changed);
|
||||
processThemeResources(fullThemeOptions, console);
|
||||
}
|
||||
}
|
||||
server.watcher.on('add', handleThemeFileCreateDelete);
|
||||
server.watcher.on('unlink', handleThemeFileCreateDelete);
|
||||
},
|
||||
handleHotUpdate(context) {
|
||||
const contextPath = path.resolve(context.file);
|
||||
const themePath = path.resolve(themeFolder);
|
||||
if (contextPath.startsWith(themePath)) {
|
||||
const changed = path.relative(themePath, contextPath);
|
||||
|
||||
console.debug('Theme file changed', changed);
|
||||
|
||||
if (changed.startsWith(settings.themeName)) {
|
||||
processThemeResources(fullThemeOptions, console);
|
||||
}
|
||||
}
|
||||
},
|
||||
async resolveId(id, importer) {
|
||||
// force theme generation if generated theme sources does not yet exist
|
||||
// this may happen for example during Java hot reload when updating
|
||||
// @Theme annotation value
|
||||
if (
|
||||
path.resolve(themeOptions.frontendGeneratedFolder, 'theme.js') === importer &&
|
||||
!existsSync(path.resolve(themeOptions.frontendGeneratedFolder, id))
|
||||
) {
|
||||
console.debug('Generate theme file ' + id + ' not existing. Processing theme resource');
|
||||
processThemeResources(fullThemeOptions, console);
|
||||
return;
|
||||
}
|
||||
if (!id.startsWith(settings.themeFolder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const location of [themeResourceFolder, frontendFolder]) {
|
||||
const result = await this.resolve(path.resolve(location, id));
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
},
|
||||
async transform(raw, id, options) {
|
||||
// rewrite urls for the application theme css files
|
||||
const [bareId, query] = id.split('?');
|
||||
if (
|
||||
(!bareId?.startsWith(themeFolder) && !bareId?.startsWith(themeOptions.themeResourceFolder)) ||
|
||||
!bareId?.endsWith('.css')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const [themeName] = bareId.substring(themeFolder.length + 1).split('/');
|
||||
return rewriteCssUrls(raw, path.dirname(bareId), path.resolve(themeFolder, themeName), console, opts);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function runWatchDog(watchDogPort, watchDogHost) {
|
||||
const client = net.Socket();
|
||||
client.setEncoding('utf8');
|
||||
client.on('error', function (err) {
|
||||
console.log('Watchdog connection error. Terminating vite process...', err);
|
||||
client.destroy();
|
||||
process.exit(0);
|
||||
});
|
||||
client.on('close', function () {
|
||||
client.destroy();
|
||||
runWatchDog(watchDogPort, watchDogHost);
|
||||
});
|
||||
|
||||
client.connect(watchDogPort, watchDogHost || 'localhost');
|
||||
}
|
||||
|
||||
let spaMiddlewareForceRemoved = false;
|
||||
|
||||
const allowedFrontendFolders = [frontendFolder, nodeModulesFolder];
|
||||
|
||||
function showRecompileReason(): PluginOption {
|
||||
return {
|
||||
name: 'vaadin:why-you-compile',
|
||||
handleHotUpdate(context) {
|
||||
console.log('Recompiling because', context.file, 'changed');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const DEV_MODE_START_REGEXP = /\/\*[\*!]\s+vaadin-dev-mode:start/;
|
||||
const DEV_MODE_CODE_REGEXP = /\/\*[\*!]\s+vaadin-dev-mode:start([\s\S]*)vaadin-dev-mode:end\s+\*\*\//i;
|
||||
|
||||
function preserveUsageStats() {
|
||||
return {
|
||||
name: 'vaadin:preserve-usage-stats',
|
||||
|
||||
transform(src: string, id: string) {
|
||||
if (id.includes('vaadin-usage-statistics')) {
|
||||
if (src.includes('vaadin-dev-mode:start')) {
|
||||
const newSrc = src.replace(DEV_MODE_START_REGEXP, '/*! vaadin-dev-mode:start');
|
||||
if (newSrc === src) {
|
||||
console.error('Comment replacement failed to change anything');
|
||||
} else if (!newSrc.match(DEV_MODE_CODE_REGEXP)) {
|
||||
console.error('New comment fails to match original regexp');
|
||||
} else {
|
||||
return { code: newSrc };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { code: src };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const vaadinConfig: UserConfigFn = (env) => {
|
||||
const devMode = env.mode === 'development';
|
||||
const productionMode = !devMode && !devBundle
|
||||
|
||||
if (devMode && process.env.watchDogPort) {
|
||||
// Open a connection with the Java dev-mode handler in order to finish
|
||||
// vite when it exits or crashes.
|
||||
runWatchDog(process.env.watchDogPort, process.env.watchDogHost);
|
||||
}
|
||||
|
||||
return {
|
||||
root: frontendFolder,
|
||||
base: '',
|
||||
publicDir: false,
|
||||
resolve: {
|
||||
alias: {
|
||||
'@vaadin/flow-frontend': jarResourcesFolder,
|
||||
Frontend: frontendFolder
|
||||
},
|
||||
preserveSymlinks: true
|
||||
},
|
||||
define: {
|
||||
OFFLINE_PATH: settings.offlinePath,
|
||||
VITE_ENABLED: 'true'
|
||||
},
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
strictPort: true,
|
||||
fs: {
|
||||
allow: allowedFrontendFolders
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: buildOutputFolder,
|
||||
emptyOutDir: devBundle,
|
||||
assetsDir: 'VAADIN/build',
|
||||
rollupOptions: {
|
||||
input: {
|
||||
indexhtml: projectIndexHtml,
|
||||
|
||||
...(hasExportedWebComponents ? { webcomponenthtml: path.resolve(frontendFolder, 'web-component.html') } : {})
|
||||
},
|
||||
onwarn: (warning: rollup.RollupWarning, defaultHandler: rollup.WarningHandler) => {
|
||||
const ignoreEvalWarning = [
|
||||
'generated/jar-resources/FlowClient.js',
|
||||
'generated/jar-resources/vaadin-spreadsheet/spreadsheet-export.js',
|
||||
'@vaadin/charts/src/helpers.js'
|
||||
];
|
||||
if (warning.code === 'EVAL' && warning.id && !!ignoreEvalWarning.find((id) => warning.id.endsWith(id))) {
|
||||
return;
|
||||
}
|
||||
defaultHandler(warning);
|
||||
}
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
entries: [
|
||||
// Pre-scan entrypoints in Vite to avoid reloading on first open
|
||||
'generated/vaadin.ts'
|
||||
],
|
||||
exclude: [
|
||||
'@vaadin/router',
|
||||
'@vaadin/vaadin-license-checker',
|
||||
'@vaadin/vaadin-usage-statistics',
|
||||
'workbox-core',
|
||||
'workbox-precaching',
|
||||
'workbox-routing',
|
||||
'workbox-strategies'
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
productionMode && brotli(),
|
||||
devMode && vaadinBundlesPlugin(),
|
||||
devMode && showRecompileReason(),
|
||||
settings.offlineEnabled && buildSWPlugin({ devMode }),
|
||||
!devMode && statsExtracterPlugin(),
|
||||
devBundle && preserveUsageStats(),
|
||||
themePlugin({ devMode }),
|
||||
postcssLit({
|
||||
include: ['**/*.css', /.*\/.*\.css\?.*/],
|
||||
exclude: [
|
||||
`${themeFolder}/**/*.css`,
|
||||
new RegExp(`${themeFolder}/.*/.*\\.css\\?.*`),
|
||||
`${themeResourceFolder}/**/*.css`,
|
||||
new RegExp(`${themeResourceFolder}/.*/.*\\.css\\?.*`),
|
||||
new RegExp('.*/.*\\?html-proxy.*')
|
||||
]
|
||||
}),
|
||||
{
|
||||
name: 'vaadin:force-remove-html-middleware',
|
||||
transformIndexHtml: {
|
||||
order: 'pre',
|
||||
handler(_html, { server }) {
|
||||
if (server && !spaMiddlewareForceRemoved) {
|
||||
server.middlewares.stack = server.middlewares.stack.filter((mw) => {
|
||||
const handleName = '' + mw.handle;
|
||||
return !handleName.includes('viteHtmlFallbackMiddleware');
|
||||
});
|
||||
spaMiddlewareForceRemoved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hasExportedWebComponents && {
|
||||
name: 'vaadin:inject-entrypoints-to-web-component-html',
|
||||
transformIndexHtml: {
|
||||
order: 'pre',
|
||||
handler(_html, { path, server }) {
|
||||
if (path !== '/web-component.html') {
|
||||
return;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: { type: 'module', src: `/generated/vaadin-web-component.ts` },
|
||||
injectTo: 'head'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'vaadin:inject-entrypoints-to-index-html',
|
||||
transformIndexHtml: {
|
||||
order: 'pre',
|
||||
handler(_html, { path, server }) {
|
||||
if (path !== '/index.html') {
|
||||
return;
|
||||
}
|
||||
|
||||
const scripts = [];
|
||||
|
||||
if (devMode) {
|
||||
scripts.push({
|
||||
tag: 'script',
|
||||
attrs: { type: 'module', src: `/generated/vite-devmode.ts` },
|
||||
injectTo: 'head'
|
||||
});
|
||||
}
|
||||
scripts.push({
|
||||
tag: 'script',
|
||||
attrs: { type: 'module', src: '/generated/vaadin.ts' },
|
||||
injectTo: 'head'
|
||||
});
|
||||
return scripts;
|
||||
}
|
||||
}
|
||||
},
|
||||
checker({
|
||||
typescript: true
|
||||
}),
|
||||
productionMode && visualizer({ brotliSize: true, filename: bundleSizeFile })
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
export const overrideVaadinConfig = (customConfig: UserConfigFn) => {
|
||||
return defineConfig((env) => mergeConfig(vaadinConfig(env), customConfig(env)));
|
||||
};
|
||||
function getVersion(module: string): string {
|
||||
const packageJson = path.resolve(nodeModulesFolder, module, 'package.json');
|
||||
return JSON.parse(readFileSync(packageJson, { encoding: 'utf-8' })).version;
|
||||
}
|
||||
function getCvdlName(module: string): string {
|
||||
const packageJson = path.resolve(nodeModulesFolder, module, 'package.json');
|
||||
return JSON.parse(readFileSync(packageJson, { encoding: 'utf-8' })).cvdlName;
|
||||
}
|
||||
Reference in New Issue
Block a user