Compare commits

...

2 Commits

135 changed files with 21749 additions and 9 deletions

View File

@ -1,4 +0,0 @@

View File

@ -29,7 +29,7 @@ public class WebLauncherLocal extends WebLauncher {
TrackResource.class, DataResource.class);
final AnalyzeApi api = new AnalyzeApi();
api.addAllApi(listOfResources);
TsGenerateApi.generateApi(api, "../front/src/app/back-api/");
TsGenerateApi.generateApi(api, "../front2/src/back-api/");
LOGGER.info("Generate APIs (DONE)");
}
@ -49,6 +49,7 @@ public class WebLauncherLocal extends WebLauncher {
ConfigBaseVariable.apiAdress = "http://0.0.0.0:19080/karusic/api/";
//ConfigBaseVariable.ssoAdress = "https://atria-soft.org/karso/api/";
ConfigBaseVariable.dbPort = "3906";
ConfigBaseVariable.testMode = "true";
}
try {
super.migrateDB();

View File

@ -1,9 +1,12 @@
package org.kar.karusic.api;
import java.util.List;
import java.util.Map;
import org.kar.archidata.dataAccess.DataAccess;
import org.kar.archidata.filter.GenericContext;
import org.kar.karusic.api.UserResourceModel.PartRight;
import org.kar.karusic.api.UserResourceModel.UserMe;
import org.kar.karusic.model.UserKarusic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -20,7 +23,7 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
@Path("/users")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
private static final Logger LOGGER = LoggerFactory.getLogger(UserResource.class);
@ -74,10 +77,15 @@ public class UserResource {
@GET
@Path("me")
@RolesAllowed("USER")
public UserOut getMe(@Context final SecurityContext sc) {
public UserMe getMe(@Context final SecurityContext sc) {
LOGGER.debug("getMe()");
final GenericContext gc = (GenericContext) sc.getUserPrincipal();
LOGGER.debug("== USER ? {}", gc.userByToken);
return new UserOut(gc.userByToken.id, gc.userByToken.name);
return new UserMe(gc.userByToken.id, gc.userByToken.name, //
Map.of(gc.userByToken.name, //
Map.of("admin", PartRight.READ_WRITE, //
"user", PartRight.READ_WRITE), //
"karusic", //
Map.of("user", PartRight.READ)));
}
}

View File

@ -0,0 +1,12 @@
package org.kar.karusic.api.UserResourceModel;
import java.util.HashMap;
import org.kar.archidata.annotation.NoWriteSpecificMode;
@NoWriteSpecificMode
public class ModuleAuthorizations extends HashMap<String, PartRight> {
private static final long serialVersionUID = 1L;
public ModuleAuthorizations() {}
}

View File

@ -0,0 +1,29 @@
package org.kar.karusic.api.UserResourceModel;
import com.fasterxml.jackson.annotation.JsonValue;
public enum PartRight {
READ(1), //
WRITE(2), //
READ_WRITE(3);
private final int value;
PartRight(final int value) {
this.value = value;
}
@JsonValue
public int getValue() {
return this.value;
}
public static PartRight fromValue(final int value) {
for (final PartRight species : PartRight.values()) {
if (species.getValue() == value) {
return species;
}
}
throw new IllegalArgumentException("PartRight: Unknown value: " + value);
}
}

View File

@ -0,0 +1,24 @@
package org.kar.karusic.api.UserResourceModel;
import java.util.Map;
import org.kar.archidata.annotation.NoWriteSpecificMode;
import io.swagger.v3.oas.annotations.media.Schema;
@NoWriteSpecificMode
public class UserMe {
public long id;
public String login;
@Schema(description = "Map<EntityName, Map<PartName, Right>>")
public Map<String, Map<String, PartRight>> rights;
public UserMe() {}
public UserMe(final long id, final String login, final Map<String, Map<String, PartRight>> rights) {
this.id = id;
this.login = login;
this.rights = rights;
}
}

View File

@ -0,0 +1,31 @@
services:
kar_db_service:
image: mysql:latest
restart: always
environment:
- MYSQL_ROOT_PASSWORD=base_db_password
volumes:
- ./data:/var/lib/mysql
mem_limit: 300m
ports:
- 3906:3306
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 10s
retries: 5
# perform a 1 minute grace to let the DB to perform the initialization
start_period: 1m
start_interval: 1m
kar_adminer_service:
image: adminer:latest
restart: always
depends_on:
kar_db_service:
condition: service_healthy
links:
- kar_db_service:db
ports:
- 4079:8080
mem_limit: 50m

View File

@ -36,4 +36,4 @@
"defer"
]
}
}
}

27
front2/.storybook/main.ts Normal file
View File

@ -0,0 +1,27 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
framework: {
name: '@storybook/react-vite',
options: {},
},
core: {
disableTelemetry: true,
builder: '@storybook/builder-vite',
},
stories: ['../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
staticDirs: ['../public'],
typescript: {
reactDocgen: false,
},
docs: {},
};
export default config;

View File

@ -0,0 +1,16 @@
<style>
html {
background: transparent !important;
}
.docs-story > :first-child {
padding: 0;
}
.docs-story > * {
background: transparent !important;
}
#root #start-ui-storybook-wrapper {
min-height: 100vh;
}
</style>

View File

@ -0,0 +1,43 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { ChakraProvider } from '@chakra-ui/react';
import { MemoryRouter } from 'react-router-dom';
import theme from '../src/theme';
// .storybook/preview.js
export const parameters = {
options: {
storySort: {
order: ['StyleGuide', 'Components', 'Fields', 'App Layout'],
},
},
actions: {},
layout: 'fullscreen',
backgrounds: { disable: true, grid: { disable: true } },
chakra: {
theme,
},
};
const DocumentationWrapper = ({ children }) => {
return (
<Box id="start-ui-storybook-wrapper" p="4" pb="8" flex="1">
{children}
</Box>
);
};
export const decorators = [
(Story, context) => (
<ChakraProvider theme={theme}>
{/* Using MemoryRouter to avoid route clashing with Storybook */}
<MemoryRouter>
<DocumentationWrapper>
<Story {...context} />
</DocumentationWrapper>
</MemoryRouter>
</ChakraProvider>
),
];

2
front2/LICENSE Normal file
View File

@ -0,0 +1,2 @@
Proprietary
@copyright Edouard Dupin 2024

6
front2/app-build.json Normal file
View File

@ -0,0 +1,6 @@
{
"display": "__DEVELOPMENT__",
"version": "__VERSION__",
"commit": "__COMMIT__",
"date": "__DATE__"
}

25
front2/build.js Normal file
View File

@ -0,0 +1,25 @@
const dayjs = require('dayjs');
const fs = require('fs');
const generateAppBuild = () => {
const getVersion = () => fs.readFileSync('version.txt', 'utf8');
const commit = process.env.VERCEL_GIT_COMMIT_SHA
? process.env.VERCEL_GIT_COMMIT_SHA
: getVersion();
const appBuildContent = {
display: `${dayjs().format('YYYY-MM-DD')}`,
version: `${commit} - ${dayjs().format()}`,
commit,
date: dayjs().format(),
};
fs.writeFileSync(
'./app-build.json',
JSON.stringify(appBuildContent, null, 2)
);
};
generateAppBuild();

0
front2/doc/.keep Normal file
View File

View File

@ -0,0 +1,2 @@
# URL for database connection
VITE_API_BASE_URL=api/

View File

@ -0,0 +1,103 @@
###############################################################
## Install dependency:
###############################################################
FROM node:latest AS dependency
# For pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
# copy the credential
COPY npmrc /root/.npmrc
COPY package.json pnpm-lock.yaml ./
COPY src/theme ./src/theme
# TODO: install only the production environment:
RUN pnpm install --prod=false
###############################################################
## Install sources
###############################################################
FROM dependency AS load_sources
# JUST to get the vertion of the application and his sha...
COPY build.js \
version.txt \
tsconfig.json \
tsconfig.node.json \
vite.config.mts \
.env.validator.js \
index.html \
./
COPY public public
COPY src src
#We are not in prod mode ==> we need to overwrite the production env.
ARG env=docker/.env.production
COPY ${env} .env
###############################################################
## Run the linter
###############################################################
FROM load_sources AS check
COPY .eslintrc.json app-build.json ./
# Run linter
RUN pnpm lint .
RUN pnpm tsc --noEmit
###############################################################
## Run the Unit test
###############################################################
FROM load_sources AS unittest
COPY vitest.config.mts app-build.json ./
# Run unit test
RUN pnpm test
###############################################################
## Build the story-book
###############################################################
FROM load_sources AS builder_storybook
COPY app-build.json ./app-build.json
COPY .storybook ./.storybook/
# build the storybook in static
RUN SKIP_ENV_VALIDATIONS=1 pnpm storybook:build
###############################################################
## Build the sources
###############################################################
FROM load_sources AS builder
# build in bundle mode all the application
RUN pnpm static:build
###############################################################
## Runner environment:
###############################################################
FROM httpd:latest AS runner
WORKDIR /app
# configure HTTP server (add a redirection on the index.html to manage new app model to re-find the generic page):
RUN sed -e '/DocumentRoot/,/Directory>/d' -i /usr/local/apache2/conf/httpd.conf
RUN sed -r 's|#LoadModule rewrite_module|LoadModule rewrite_module|' -i /usr/local/apache2/conf/httpd.conf
RUN echo '<VirtualHost *:80> \n\
ServerName my-app \n\
DocumentRoot "/usr/local/apache2/htdocs" \n\
<Directory "/usr/local/apache2/htdocs"> \n\
Options Indexes FollowSymLinks \n\
AllowOverride None \n\
Require all granted \n\
RewriteEngine on \n\
# Do not rewrite files or directories \n\
RewriteCond %{REQUEST_FILENAME} -f [OR] \n\
RewriteCond %{REQUEST_FILENAME} -d \n\
RewriteRule ^ - [L] \n\
# Rewrite everything else to index.html to allow HTML5 state links \n\
RewriteRule ^ app/index.html [L] \n\
</Directory> \n\
</VirtualHost> \n\
' >> /usr/local/apache2/conf/httpd.conf
# copy artifact build from the 'build environment'
COPY --from=builder /app/dist /usr/local/apache2/htdocs/app

13
front2/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Karusic</title>
<link rel="icon" href="/favicon.ico" />
</head>
<body style="width:100vw;height:100vh;min-width:100%;min-height:100%;">
<div id="root" style="width:100%;height:100%;min-width:100%;min-height:100%;"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

18
front2/knip.ts Normal file
View File

@ -0,0 +1,18 @@
import type { KnipConfig } from 'knip';
const config: KnipConfig = {
// Ignoring mostly shell binaries
ignoreBinaries: ['export', 'sleep'],
ignore: [
// Related to tests
'tests/**',
'**.conf.js',
'steps.d.ts',
'steps_file.js',
'env_ci/codecept.conf.js',
// Generic components are useful.
'src/components/**',
],
};
export default config;

108
front2/package.json Normal file
View File

@ -0,0 +1,108 @@
{
"name": "karusic",
"private": true,
"version": "0.0.1",
"description": "KAR web music application",
"author": {
"name": "Edouard DUPIN",
"email": "yui.heero@gmail.farm"
},
"license": "PROPRIETARY",
"engines": {
"node": ">=20"
},
"scripts": {
"update_packages": "ncu --upgrade",
"install_dependency": "pnpm install",
"test": "vitest run",
"test:watch": "vitest watch",
"build": "tsc && vite build",
"dev": "vite",
"pretty": "prettier -w .",
"lint": "pnpm tsc --noEmit",
"storybook": "storybook dev -p 3001",
"storybook:build": "storybook build && mv ./storybook-static ./public/storybook"
},
"lint-staged": {
"*.{ts,tsx,js,jsx,json}": "prettier --write"
},
"dependencies": {
"@chakra-ui/anatomy": "2.2.2",
"@chakra-ui/cli": "2.4.1",
"@chakra-ui/react": "2.8.2",
"@chakra-ui/theme-tools": "2.1.2",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@emotion/react": "11.13.0",
"@emotion/styled": "11.13.0",
"allotment": "1.20.2",
"css-mediaquery": "0.1.2",
"dayjs": "1.11.12",
"history": "5.3.0",
"react": "18.3.1",
"react-color-palette": "7.2.2",
"react-currency-input-field": "3.8.0",
"react-custom-scrollbars": "4.2.1",
"react-day-picker": "9.0.8",
"react-dom": "18.3.1",
"react-error-boundary": "4.0.13",
"react-focus-lock": "2.12.1",
"react-icons": "5.3.0",
"react-popper": "2.3.0",
"react-router-dom": "6.26.1",
"react-select": "5.8.0",
"react-simple-keyboard": "3.7.144",
"react-sticky-el": "2.1.0",
"react-use": "17.5.1",
"react-use-draggable-scroll": "0.4.7",
"react-virtuoso": "4.10.1",
"ts-pattern": "5.3.1",
"uuid": "10.0.0",
"zod": "3.23.8",
"zustand": "4.5.5"
},
"devDependencies": {
"@chakra-ui/styled-system": "2.9.2",
"@playwright/test": "1.46.0",
"@storybook/addon-actions": "8.2.9",
"@storybook/addon-essentials": "8.2.9",
"@storybook/addon-links": "8.2.9",
"@storybook/addon-mdx-gfm": "8.2.9",
"@storybook/react": "8.2.9",
"@storybook/react-vite": "8.2.9",
"@storybook/theming": "8.2.9",
"@testing-library/jest-dom": "6.4.8",
"@testing-library/react": "16.0.0",
"@testing-library/user-event": "14.5.2",
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"@types/jest": "29.5.12",
"@types/node": "22.3.0",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-sticky-el": "1.0.7",
"@typescript-eslint/eslint-plugin": "8.1.0",
"@typescript-eslint/parser": "8.1.0",
"@vitejs/plugin-react": "4.3.1",
"eslint": "9.9.0",
"eslint-plugin-codeceptjs": "1.3.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-storybook": "0.8.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"knip": "5.27.2",
"lint-staged": "15.2.9",
"prettier": "3.3.3",
"puppeteer": "23.1.0",
"react-is": "18.3.1",
"storybook": "8.2.9",
"ts-node": "10.9.2",
"typescript": "5.5.4",
"vite": "5.4.1",
"vitest": "2.0.5",
"npm-check-updates": "^17.0.6"
}
}

View File

13977
front2/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

16
front2/prettier.config.js Normal file
View File

@ -0,0 +1,16 @@
// Using a JS file, allowing us to add comments
module.exports = {
// This plugins line is mandatory for the plugin to work with pnpm.
// https://github.com/trivago/prettier-plugin-sort-imports/blob/61d069711008c530f5a41ca4e254781abc5de358/README.md?plain=1#L89-L96
plugins: ['@trivago/prettier-plugin-sort-imports'],
endOfLine: 'lf',
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
arrowParens: 'always',
importOrder: ['^react$', '^(?!^react$|^@/|^[./]).*', '^@/(.*)$', '^[./]'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
importOrderParserPlugins: ['jsx', 'typescript'],
};

BIN
front2/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

118
front2/src/App.tsx Normal file
View File

@ -0,0 +1,118 @@
import { useState } from 'react';
import { ChakraProvider, Select } from '@chakra-ui/react';
import {
Box,
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { environment } from '@/environment';
import { App as SpaApp } from '@/scene/App';
import { USERS } from '@/service/session';
import theme from '@/theme';
import { hashLocalData } from '@/utils/sso';
const AppEnvHint = () => {
const modal = useDisclosure();
const [selectUserTest, setSelectUserTest] = useState<string>('NO_USER');
//const setUser = useRightsStore((store) => store.setUser);
const buildEnv =
process.env.NODE_ENV === 'development'
? 'Development'
: import.meta.env.VITE_DEV_ENV_NAME;
const envName: Array<string> = [];
!!buildEnv && envName.push(buildEnv);
if (!envName.length) {
return null;
}
const handleChange = (selectedOption) => {
console.log(`SELECT: [${selectedOption.target.value}]`);
setSelectUserTest(selectedOption.target.value);
};
const onClose = () => {
modal.onClose();
if (selectUserTest == 'NO_USER') {
window.location.href = `/${environment.applName}/sso/${hashLocalData()}/false/__LOGOUT__`;
} else {
window.location.href = `/${environment.applName}/sso/${hashLocalData()}/true/${USERS[selectUserTest]}`;
}
};
return (
<>
<Box
zIndex="100000"
position="fixed"
top="0"
insetStart="0"
insetEnd="0"
h="2px"
bg="warning.400"
as="button"
cursor="pointer"
data-test-id="devtools"
onClick={modal.onOpen}
>
<Text
position="fixed"
top="0"
insetStart="4"
bg="warning.400"
color="warning.900"
fontSize="0.6rem"
fontWeight="bold"
px="10px"
marginLeft="25%"
borderBottomStartRadius="sm"
borderBottomEndRadius="sm"
textTransform="uppercase"
>
{envName.join(' : ')}
</Text>
</Box>
<Modal isOpen={modal.isOpen} onClose={modal.onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Outils développeurs</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Stack>
<Text>Utilisateur</Text>
<Select placeholder="Select test user" onChange={handleChange}>
{Object.keys(USERS).map((key) => (
<option value={key} key={key}>
{key}
</option>
))}
</Select>
</Stack>
</ModalBody>
<ModalFooter>
<Button onClick={onClose}>Apply</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
const App = () => {
return (
<ChakraProvider theme={theme}>
<AppEnvHint />
<SpaApp />
</ChakraProvider>
);
};
export default App;

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="499"
height="498"
viewBox="0 0 499 498"
version="1.1"
id="svg10"
sodipodi:docname="avatar_generic.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs14" />
<sodipodi:namedview
id="namedview12"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.5060241"
inkscape:cx="344.97766"
inkscape:cy="288.78866"
inkscape:window-width="3838"
inkscape:window-height="2118"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="svg10" />
<rect
style="fill:#4c83e5;fill-opacity:1;stroke:none;stroke-width:8.82841587;stroke-opacity:1"
width="500"
height="500"
x="0"
y="-1.9999847"
id="rect2" />
<path
style="fill:#4e4e4d;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="m 198.78572,292.40268 -11.97951,28.10434 -7.03868,-39.62542 -32.77188,20.51375 14.30166,-59.1095 -47.70972,20.8692 30.95257,-58.44261 -40.08325,-11.9709 50.99682,-31.08004 -30.32488,-33.92052 41.38608,6.88643 c 0,0 -27.42157,-58.130582 -26.08007,-58.130582 1.34149,0 54.85161,37.962212 54.85161,37.962212 l 6.10019,-52.959427 22.55992,46.026947 33.20732,-51.718401 8.75817,54.113371 53.78031,-55.134502 -14.88381,76.635492 112.00146,-17.67965 -84.26404,54.10353 65.61018,10.26713 -53.91421,37.51917 40.05564,55.00796 -51.48529,-23.57551 7.49544,56.99322 -27.79947,-24.64556 -3.80452,36.62241 -23.37869,-22.14564 -5.89389,23.82189 -20.64297,-29.48769 -15.46209,46.92367 -7.2608,-54.46889 -21.32424,51.07849 z"
id="path3162"
sodipodi:nodetypes="cccccccccccsccccccccccccccccccccccc" />
<path
style="fill:#fbd0a6;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 126.90172,351.92657 55.90379,145.23242 168.77912,0.48055 8.10502,-147.83696 -56.19339,-10.73582 -9.91368,3.09728 -8.25753,-48.27446 -8.77147,-41.82541 -73.97306,-0.86753 -4.65072,84.85557 z"
id="path3467"
sodipodi:nodetypes="ccccccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 45.319305,524.24985 50.878125,-168.64351 91.02866,-21.85078 124.81002,189.43887 15.4976,-12.33208 -130.63444,-191.36234 -9.67318,14.25555 124.81002,189.43887"
id="path275"
sodipodi:nodetypes="cccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 443.88287,524.02559 393.00474,355.38208 301.97608,333.5313 177.16606,522.97017 161.66846,510.63809 292.3029,319.27575 301.97608,333.5313 177.16606,522.97017"
id="path275-6"
sodipodi:nodetypes="cccccccc" />
<path
style="fill:#fbd0a6;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 187.52213,139.10279 14.34593,25.22547 9.57434,-32.06638 13.85516,33.79118 18.44245,-38.89028 4.92331,44.11511 20.28515,-38.50102 -2.21466,39.35905 31.27764,-28.90273 -5.47875,45.83312 23.27252,-10.25342 -8.67174,30.59353 c 24.86464,-33.77835 23.21015,12.27629 3.94365,35.3922 l -12.95127,26.98572 -12.80079,22.10524 -26.61623,18.28625 -53.44338,-20.79546 c -21.13665,-20.42844 -23.443,-25.48798 -31.95313,-51.61993 -19.36867,-32.18928 -17.20493,-59.66994 4.27858,-38.00437 l -2.30548,-27.96686 11.07502,10.22035 -14.60569,-33.15484 22.1057,18.70679 z"
id="path9531"
sodipodi:nodetypes="cccccccccccccccccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="256" height="256" viewBox="0 0 67.733333 67.733333" version="1.1" id="svg8"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)" sodipodi:docname="ikon_gray.svg"
inkscape:export-filename="/home/heero/dev/perso/appl_pro/NoKomment/plugin/chrome/ikon.png"
inkscape:export-xdpi="7.1250005" inkscape:export-ydpi="7.1250005"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs id="defs2">
<filter style="color-interpolation-filters:sRGB;" inkscape:label="Drop Shadow" id="filter5338" x="-0.12319682"
y="-0.081815216" width="1.2463936" height="1.1636304">
<feFlood flood-opacity="1" flood-color="rgb(0,255,0)" result="flood" id="feFlood5328" />
<feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1" id="feComposite5330" />
<feGaussianBlur in="composite1" stdDeviation="2.1" result="blur" id="feGaussianBlur5332" />
<feOffset dx="0" dy="0" result="offset" id="feOffset5334" />
<feComposite in="SourceGraphic" in2="offset" operator="over" result="composite2" id="feComposite5336" />
</filter>
<filter inkscape:collect="always" style="color-interpolation-filters:sRGB" id="filter1159" x="-0.11802406"
width="1.2360481" y="-0.078379973" height="1.1567599">
<feGaussianBlur inkscape:collect="always" stdDeviation="2.0118255" id="feGaussianBlur1161" />
</filter>
<filter style="color-interpolation-filters:sRGB" inkscape:label="Drop Shadow" id="filter5338-3" x="-0.12319682"
y="-0.081815216" width="1.2463936" height="1.1636304">
<feFlood flood-opacity="1" flood-color="rgb(0,255,0)" result="flood" id="feFlood5328-6" />
<feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1" id="feComposite5330-7" />
<feGaussianBlur in="composite1" stdDeviation="2.1" result="blur" id="feGaussianBlur5332-5" />
<feOffset dx="0" dy="0" result="offset" id="feOffset5334-3" />
<feComposite in="SourceGraphic" in2="offset" operator="over" result="composite2" id="feComposite5336-5" />
</filter>
</defs>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="4" inkscape:cx="27.125" inkscape:cy="217.5"
inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="true" units="px"
inkscape:snap-text-baseline="false" inkscape:window-width="3838" inkscape:window-height="2118"
inkscape:window-x="0" inkscape:window-y="20" inkscape:window-maximized="1" inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
<inkscape:grid type="xygrid" id="grid4504" originx="0" originy="0" spacingy="1" spacingx="1" units="px"
visible="true" />
</sodipodi:namedview>
<metadata id="metadata5">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-229.26668)"
style="display:inline">
<g id="text821-7"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:84.55024719px;line-height:1.25;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;opacity:1;fill:#2b3137;fill-opacity:1;stroke:none;stroke-width:2.11376619;stroke-opacity:1"
transform="matrix(0.8407653,0,0,0.83753055,-37.28971,3.4402954)" aria-label="K">
<path id="path823-5"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:84.5502px;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;opacity:0.775;fill:#2b3137;fill-opacity:1;stroke-width:2.11377;filter:url(#filter5338)"
d="m 65.200545,279.95309 v 61.60223 h 8.949095 v -20.53407 l 6.392211,-6.84449 19.176632,27.37856 6.392207,-5.47534 -20.455071,-27.37918 20.455071,-21.9026 -6.392207,-6.84511 -25.568843,27.37918 v -27.37918 z m 34.623041,3.89642 0.08113,0.91996 c -0.319958,0.0205 -0.600028,0.12056 -0.843281,0.38008 -0.481327,0.51353 -0.393613,1.29347 0.321455,1.96887 0.73708,0.69622 1.51285,0.73176 2.02523,0.18511 0.24321,-0.25953 0.31335,-0.58883 0.29195,-0.94218 l 0.90904,0.0154 c 0.0722,0.61998 -0.12994,1.18923 -0.58021,1.66963 -0.83844,0.89456 -2.190053,1.07514 -3.400168,-0.0679 -1.188114,-1.12225 -1.171861,-2.52288 -0.266137,-3.48919 0.424395,-0.45279 0.991241,-0.62554 1.460989,-0.63984 z m -6.777588,6.44528 0.676714,0.6386 -0.786733,0.83975 2.228054,2.10401 0.786119,-0.83914 0.676714,0.63861 -2.333772,2.49087 -0.676714,-0.63923 0.786734,-0.83913 -2.228054,-2.10462 -0.786734,0.83913 -0.676099,-0.6386 z m -5.778189,6.97221 0.129073,0.89158 c -0.419593,0.0825 -0.731113,0.21546 -1.057173,0.56333 -0.253606,0.27057 -0.314123,0.55655 -0.105103,0.75398 0.220021,0.20783 0.524373,0.0375 0.977271,-0.18017 l 0.595582,-0.27025 c 0.615941,-0.3031 1.187271,-0.32558 1.693321,0.15241 0.599556,0.56632 0.616629,1.36433 -0.19361,2.44089 -0.677315,0.60577 -1.102122,0.82218 -1.800268,0.88108 l -0.121083,-0.98844 c 0.484299,-0.0631 0.943518,-0.25438 1.274754,-0.60776 0.320886,-0.34235 0.344427,-0.63278 0.16841,-0.79903 -0.258525,-0.24419 -0.521361,-0.0857 -0.985261,0.12155 l -0.637377,0.28198 c -0.526655,0.25209 -1.170772,0.33129 -1.693321,-0.16228 -0.594058,-0.56111 -0.565292,-1.54388 0.169639,-2.32797 0.403694,-0.4307 0.971757,-0.716 1.585146,-0.7509 z m -6.585821,6.21884 2.205312,2.08364 c 0.929589,0.87805 1.047872,1.78072 0.224957,2.65869 -0.81774,0.8725 -1.743461,0.83116 -2.67305,-0.0469 l -2.205313,-2.08364 0.765836,-0.81692 2.288288,2.16138 c 0.429042,0.40526 0.810303,0.46332 1.126013,0.12649 0.320885,-0.34235 0.244649,-0.72634 -0.184391,-1.1316 l -2.288288,-2.16138 z m -4.57965,9.20516 2.197937,0.53865 -0.853729,0.91071 -1.930571,-0.5294 -0.407503,0.43499 1.287047,1.21551 -0.760919,0.81199 -3.580867,-3.38245 1.200998,-1.28091 c 0.446394,-0.47625 0.945677,-0.80165 1.465291,-0.78175 0.311768,0.0119 0.630508,0.14843 0.950227,0.45042 0.523732,0.4947 0.617235,1.06543 0.432089,1.61224 m 4e-6,5e-5 v 0 m -1.574086,-1.01133 c -0.219842,-0.009 -0.443011,0.13842 -0.69208,0.40414 l -0.378001,0.40291 1.006773,0.95081 0.378001,-0.4029 c 0.39852,-0.42517 0.434395,-0.8287 0.08236,-1.16122 -0.134075,-0.12664 -0.265149,-0.18828 -0.397054,-0.19374 z m -7.033891,5.87578 4.63128,2.26134 -0.807017,0.86135 -1.059017,-0.58493 -1.015378,1.08286 0.645982,1.02608 -0.781816,0.8342 -2.529841,-4.50355 z m 1.278214,2.86141 0.707674,-0.7537 -1.841448,-1.03411 -0.02028,0.0222 z m 0.707674,-0.7537 0.779358,0.43005 z"
sodipodi:nodetypes="cccccccccccccccsccccscsccccccccccccccccssccssccssccssccscccccscccsccccccccscccccccccssccccccccccccccccccc" />
</g>
<g id="text821"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:84.55024719px;line-height:1.25;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#2b3137;fill-opacity:1;stroke:none;stroke-width:2.11376619;stroke-opacity:1;filter:url(#filter1159)"
transform="matrix(1.0347881,0,0,0.96638144,-54.239583,-37.041665)" aria-label="K" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1,250 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTCallbacks,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Album,
AlbumWrite,
Long,
UUID,
ZodAlbum,
isAlbum,
} from "../model";
export namespace AlbumResource {
/**
* Add a Track on a specific album
*/
export function addTrack({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
trackId: Long,
id: Long,
},
}): Promise<Album> {
return RESTRequestJson({
restModel: {
endPoint: "/album/{id}/track/{trackId}",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isAlbum);
};
/**
* Get a specific Album with his ID
*/
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<Album> {
return RESTRequestJson({
restModel: {
endPoint: "/album/{id}",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isAlbum);
};
export const ZodGetsTypeReturn = zod.array(ZodAlbum);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
/**
* Get all the available Albums
*/
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/album/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
/**
* Update a specific album
*/
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: AlbumWrite,
}): Promise<Album> {
return RESTRequestJson({
restModel: {
endPoint: "/album/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isAlbum);
};
/**
* Add an album (when all the data already exist)
*/
export function post({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: AlbumWrite,
}): Promise<Album> {
return RESTRequestJson({
restModel: {
endPoint: "/album/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
}, isAlbum);
};
/**
* Remove a specific album
*/
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/album/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
/**
* Remove a cover on a specific album
*/
export function removeCover({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
coverId: UUID,
id: Long,
},
}): Promise<Album> {
return RESTRequestJson({
restModel: {
endPoint: "/album/{id}/cover/{coverId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isAlbum);
};
/**
* Remove a Track on a specific album
*/
export function removeTrack({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
trackId: Long,
id: Long,
},
}): Promise<Album> {
return RESTRequestJson({
restModel: {
endPoint: "/album/{id}/track/{trackId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isAlbum);
};
/**
* Add a cover on a specific album
*/
export function uploadCover({
restConfig,
params,
data,
callbacks,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: {
file: File,
},
callbacks?: RESTCallbacks,
}): Promise<Album> {
return RESTRequestJson({
restModel: {
endPoint: "/album/{id}/cover",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
callbacks,
}, isAlbum);
};
}

View File

@ -0,0 +1,181 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTCallbacks,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Artist,
ArtistWrite,
Long,
UUID,
ZodArtist,
isArtist,
} from "../model";
export namespace ArtistResource {
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<Artist> {
return RESTRequestJson({
restModel: {
endPoint: "/artist/{id}",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isArtist);
};
export const ZodGetsTypeReturn = zod.array(ZodArtist);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/artist/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: ArtistWrite,
}): Promise<Artist> {
return RESTRequestJson({
restModel: {
endPoint: "/artist/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isArtist);
};
export function post({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: ArtistWrite,
}): Promise<Artist> {
return RESTRequestJson({
restModel: {
endPoint: "/artist/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
}, isArtist);
};
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/artist/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
export function removeCover({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
coverId: UUID,
id: Long,
},
}): Promise<Artist> {
return RESTRequestJson({
restModel: {
endPoint: "/artist/{id}/cover/{coverId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isArtist);
};
export function uploadCover({
restConfig,
params,
data,
callbacks,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: {
file: File,
},
callbacks?: RESTCallbacks,
}): Promise<Artist> {
return RESTRequestJson({
restModel: {
endPoint: "/artist/{id}/cover",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
callbacks,
}, isArtist);
};
}

View File

@ -0,0 +1,128 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import {
UUID,
} from "../model";
export namespace DataResource {
/**
* Get back some data from the data environment (with a beautiful name (permit download with basic name)
*/
export function retrieveDataFull({
restConfig,
queries,
params,
data,
}: {
restConfig: RESTConfig,
queries: {
Authorization?: string,
},
params: {
name: string,
uuid: UUID,
},
data: string,
}): Promise<object> {
return RESTRequestJson({
restModel: {
endPoint: "/data/{uuid}/{name}",
requestType: HTTPRequestModel.GET,
},
restConfig,
params,
queries,
data,
});
};
/**
* Get back some data from the data environment
*/
export function retrieveDataId({
restConfig,
queries,
params,
data,
}: {
restConfig: RESTConfig,
queries: {
Authorization?: string,
},
params: {
uuid: UUID,
},
data: string,
}): Promise<object> {
return RESTRequestJson({
restModel: {
endPoint: "/data/{uuid}",
requestType: HTTPRequestModel.GET,
},
restConfig,
params,
queries,
data,
});
};
/**
* Get a thumbnail of from the data environment (if resize is possible)
*/
export function retrieveDataThumbnailId({
restConfig,
queries,
params,
data,
}: {
restConfig: RESTConfig,
queries: {
Authorization?: string,
},
params: {
uuid: UUID,
},
data: string,
}): Promise<object> {
return RESTRequestJson({
restModel: {
endPoint: "/data/thumbnail/{uuid}",
requestType: HTTPRequestModel.GET,
},
restConfig,
params,
queries,
data,
});
};
/**
* Insert a new data in the data environment
*/
export function uploadFile({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: {
file: File,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/data//upload/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
},
restConfig,
data,
});
};
}

View File

@ -0,0 +1,6 @@
/**
* Interface of the server (auto-generated code)
*/
export namespace Front {
}

View File

@ -0,0 +1,181 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTCallbacks,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Gender,
GenderWrite,
Long,
UUID,
ZodGender,
isGender,
} from "../model";
export namespace GenderResource {
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<Gender> {
return RESTRequestJson({
restModel: {
endPoint: "/gender/{id}",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isGender);
};
export const ZodGetsTypeReturn = zod.array(ZodGender);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/gender/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: GenderWrite,
}): Promise<Gender> {
return RESTRequestJson({
restModel: {
endPoint: "/gender/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isGender);
};
export function post({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: GenderWrite,
}): Promise<Gender> {
return RESTRequestJson({
restModel: {
endPoint: "/gender/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
}, isGender);
};
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/gender/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
export function removeCover({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
coverId: UUID,
id: Long,
},
}): Promise<Gender> {
return RESTRequestJson({
restModel: {
endPoint: "/gender/{id}/cover/{coverId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isGender);
};
export function uploadCover({
restConfig,
params,
data,
callbacks,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: {
file: File,
},
callbacks?: RESTCallbacks,
}): Promise<Gender> {
return RESTRequestJson({
restModel: {
endPoint: "/gender/{id}/cover",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
callbacks,
}, isGender);
};
}

View File

@ -0,0 +1,32 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTConfig,
RESTRequestJson,
} from "../rest-tools";
import {
HealthResult,
isHealthResult,
} from "../model";
export namespace HealthCheck {
export function getHealth({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<HealthResult> {
return RESTRequestJson({
restModel: {
endPoint: "/health_check/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isHealthResult);
};
}

View File

@ -0,0 +1,12 @@
/**
* Interface of the server (auto-generated code)
*/
export * from "./album-resource"
export * from "./artist-resource"
export * from "./data-resource"
export * from "./front"
export * from "./gender-resource"
export * from "./health-check"
export * from "./playlist-resource"
export * from "./track-resource"
export * from "./user-resource"

View File

@ -0,0 +1,219 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Long,
Playlist,
PlaylistWrite,
UUID,
ZodPlaylist,
isPlaylist,
} from "../model";
export namespace PlaylistResource {
export function addTrack({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
trackId: Long,
id: Long,
},
}): Promise<Playlist> {
return RESTRequestJson({
restModel: {
endPoint: "/playlist/{id}/track/{trackId}",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isPlaylist);
};
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<Playlist> {
return RESTRequestJson({
restModel: {
endPoint: "/playlist/{id}",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isPlaylist);
};
export const ZodGetsTypeReturn = zod.array(ZodPlaylist);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/playlist/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: PlaylistWrite,
}): Promise<Playlist> {
return RESTRequestJson({
restModel: {
endPoint: "/playlist/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isPlaylist);
};
export function post({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: PlaylistWrite,
}): Promise<Playlist> {
return RESTRequestJson({
restModel: {
endPoint: "/playlist/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
}, isPlaylist);
};
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/playlist/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
export function removeCover({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
coverId: UUID,
id: Long,
},
}): Promise<Playlist> {
return RESTRequestJson({
restModel: {
endPoint: "/playlist/{id}/cover/{coverId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isPlaylist);
};
export function removeTrack({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
trackId: Long,
id: Long,
},
}): Promise<Playlist> {
return RESTRequestJson({
restModel: {
endPoint: "/playlist/{id}/track/{trackId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isPlaylist);
};
export function uploadCover({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: {
file: File,
},
}): Promise<Playlist> {
return RESTRequestJson({
restModel: {
endPoint: "/playlist/{id}/cover",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isPlaylist);
};
}

View File

@ -0,0 +1,252 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTCallbacks,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Long,
Track,
TrackWrite,
UUID,
ZodTrack,
isTrack,
} from "../model";
export namespace TrackResource {
export function addTrack({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
artistId: Long,
id: Long,
},
}): Promise<Track> {
return RESTRequestJson({
restModel: {
endPoint: "/track/{id}/artist/{artistId}",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isTrack);
};
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<Track> {
return RESTRequestJson({
restModel: {
endPoint: "/track/{id}",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isTrack);
};
export const ZodGetsTypeReturn = zod.array(ZodTrack);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/track/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: TrackWrite,
}): Promise<Track> {
return RESTRequestJson({
restModel: {
endPoint: "/track/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isTrack);
};
export function post({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: TrackWrite,
}): Promise<Track> {
return RESTRequestJson({
restModel: {
endPoint: "/track/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
}, isTrack);
};
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/track/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
export function removeCover({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
coverId: UUID,
id: Long,
},
}): Promise<Track> {
return RESTRequestJson({
restModel: {
endPoint: "/track/{id}/cover/{coverId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isTrack);
};
export function removeTrack({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
artistId: Long,
id: Long,
},
}): Promise<Track> {
return RESTRequestJson({
restModel: {
endPoint: "/track/{id}/artist/{trackId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isTrack);
};
export function uploadCover({
restConfig,
params,
data,
callbacks,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: {
file: File,
},
callbacks?: RESTCallbacks,
}): Promise<Track> {
return RESTRequestJson({
restModel: {
endPoint: "/track/{id}/cover",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
callbacks,
}, isTrack);
};
export function uploadTrack({
restConfig,
data,
callbacks,
}: {
restConfig: RESTConfig,
data: {
fileName: string,
file: File,
gender: string,
artist: string,
album: string,
trackId: Long,
title: string,
},
callbacks?: RESTCallbacks,
}): Promise<Track> {
return RESTRequestJson({
restModel: {
endPoint: "/track/upload/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
callbacks,
}, isTrack);
};
}

View File

@ -0,0 +1,84 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTConfig,
RESTRequestJson,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Long,
UserKarusic,
UserMe,
ZodUserKarusic,
isUserKarusic,
isUserMe,
} from "../model";
export namespace UserResource {
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<UserKarusic> {
return RESTRequestJson({
restModel: {
endPoint: "/users/{id}",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isUserKarusic);
};
export function getMe({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<UserMe> {
return RESTRequestJson({
restModel: {
endPoint: "/users/me",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isUserMe);
};
export const ZodGetsTypeReturn = zod.array(ZodUserKarusic);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/users/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
}

View File

@ -0,0 +1,7 @@
/**
* Interface of the server (auto-generated code)
*/
export * from "./model";
export * from "./api";
export * from "./rest-tools";

View File

@ -0,0 +1,53 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodUUID} from "./uuid";
import {ZodLocalDate} from "./local-date";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodAlbum = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(),
description: zod.string().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).optional(),
publication: ZodLocalDate.optional(),
});
export type Album = zod.infer<typeof ZodAlbum>;
export function isAlbum(data: any): data is Album {
try {
ZodAlbum.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodAlbum' error=${e}`);
return false;
}
}
export const ZodAlbumWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
publication: ZodLocalDate.nullable().optional(),
});
export type AlbumWrite = zod.infer<typeof ZodAlbumWrite>;
export function isAlbumWrite(data: any): data is AlbumWrite {
try {
ZodAlbumWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodAlbumWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,59 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodUUID} from "./uuid";
import {ZodLocalDate} from "./local-date";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodArtist = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(),
description: zod.string().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).optional(),
firstName: zod.string().max(256).optional(),
surname: zod.string().max(256).optional(),
birth: ZodLocalDate.optional(),
death: ZodLocalDate.optional(),
});
export type Artist = zod.infer<typeof ZodArtist>;
export function isArtist(data: any): data is Artist {
try {
ZodArtist.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodArtist' error=${e}`);
return false;
}
}
export const ZodArtistWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
firstName: zod.string().max(256).nullable().optional(),
surname: zod.string().max(256).nullable().optional(),
birth: ZodLocalDate.nullable().optional(),
death: ZodLocalDate.nullable().optional(),
});
export type ArtistWrite = zod.infer<typeof ZodArtistWrite>;
export function isArtistWrite(data: any): data is ArtistWrite {
try {
ZodArtistWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodArtistWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,50 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodUUID} from "./uuid";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodGender = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(),
description: zod.string().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).optional(),
});
export type Gender = zod.infer<typeof ZodGender>;
export function isGender(data: any): data is Gender {
try {
ZodGender.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGender' error=${e}`);
return false;
}
}
export const ZodGenderWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
});
export type GenderWrite = zod.infer<typeof ZodGenderWrite>;
export function isGenderWrite(data: any): data is GenderWrite {
try {
ZodGenderWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenderWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,41 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodGenericData, ZodGenericDataWrite } from "./generic-data";
export const ZodGenericDataSoftDelete = ZodGenericData.extend({
/**
* Deleted state
*/
deleted: zod.boolean().readonly().optional(),
});
export type GenericDataSoftDelete = zod.infer<typeof ZodGenericDataSoftDelete>;
export function isGenericDataSoftDelete(data: any): data is GenericDataSoftDelete {
try {
ZodGenericDataSoftDelete.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataSoftDelete' error=${e}`);
return false;
}
}
export const ZodGenericDataSoftDeleteWrite = ZodGenericDataWrite.extend({
});
export type GenericDataSoftDeleteWrite = zod.infer<typeof ZodGenericDataSoftDeleteWrite>;
export function isGenericDataSoftDeleteWrite(data: any): data is GenericDataSoftDeleteWrite {
try {
ZodGenericDataSoftDeleteWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataSoftDeleteWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,42 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
import {ZodGenericTiming, ZodGenericTimingWrite } from "./generic-timing";
export const ZodGenericData = ZodGenericTiming.extend({
/**
* Unique Id of the object
*/
id: ZodLong.readonly(),
});
export type GenericData = zod.infer<typeof ZodGenericData>;
export function isGenericData(data: any): data is GenericData {
try {
ZodGenericData.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericData' error=${e}`);
return false;
}
}
export const ZodGenericDataWrite = ZodGenericTimingWrite.extend({
});
export type GenericDataWrite = zod.infer<typeof ZodGenericDataWrite>;
export function isGenericDataWrite(data: any): data is GenericDataWrite {
try {
ZodGenericDataWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,45 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodIsoDate} from "./iso-date";
export const ZodGenericTiming = zod.object({
/**
* Create time of the object
*/
createdAt: ZodIsoDate.readonly().optional(),
/**
* When update the object
*/
updatedAt: ZodIsoDate.readonly().optional(),
});
export type GenericTiming = zod.infer<typeof ZodGenericTiming>;
export function isGenericTiming(data: any): data is GenericTiming {
try {
ZodGenericTiming.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericTiming' error=${e}`);
return false;
}
}
export const ZodGenericTimingWrite = zod.object({
});
export type GenericTimingWrite = zod.infer<typeof ZodGenericTimingWrite>;
export function isGenericTimingWrite(data: any): data is GenericTimingWrite {
try {
ZodGenericTimingWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericTimingWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,36 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodHealthResult = zod.object({
});
export type HealthResult = zod.infer<typeof ZodHealthResult>;
export function isHealthResult(data: any): data is HealthResult {
try {
ZodHealthResult.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodHealthResult' error=${e}`);
return false;
}
}
export const ZodHealthResultWrite = zod.object({
});
export type HealthResultWrite = zod.infer<typeof ZodHealthResultWrite>;
export function isHealthResultWrite(data: any): data is HealthResultWrite {
try {
ZodHealthResultWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodHealthResultWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,23 @@
/**
* Interface of the server (auto-generated code)
*/
export * from "./album"
export * from "./artist"
export * from "./gender"
export * from "./generic-data"
export * from "./generic-data-soft-delete"
export * from "./generic-timing"
export * from "./health-result"
export * from "./int"
export * from "./iso-date"
export * from "./local-date"
export * from "./long"
export * from "./part-right"
export * from "./playlist"
export * from "./rest-error-response"
export * from "./timestamp"
export * from "./track"
export * from "./user"
export * from "./user-karusic"
export * from "./user-me"
export * from "./uuid"

View File

@ -0,0 +1,36 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const Zodint = zod.object({
});
export type int = zod.infer<typeof Zodint>;
export function isint(data: any): data is int {
try {
Zodint.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='Zodint' error=${e}`);
return false;
}
}
export const ZodintWrite = zod.object({
});
export type intWrite = zod.infer<typeof ZodintWrite>;
export function isintWrite(data: any): data is intWrite {
try {
ZodintWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodintWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodIsoDate = zod.string().datetime({ precision: 3 });
export type IsoDate = zod.infer<typeof ZodIsoDate>;

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodLocalDate = zod.string().date();
export type LocalDate = zod.infer<typeof ZodLocalDate>;

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodLong = zod.number();
export type Long = zod.infer<typeof ZodLong>;

View File

@ -0,0 +1,23 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export enum PartRight {
READ = 1,
WRITE = 2,
READ_WRITE = 3,
};
export const ZodPartRight = zod.nativeEnum(PartRight);
export function isPartRight(data: any): data is PartRight {
try {
ZodPartRight.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodPartRight' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,53 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodUUID} from "./uuid";
import {ZodLong} from "./long";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodPlaylist = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(),
description: zod.string().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).optional(),
tracks: zod.array(ZodLong),
});
export type Playlist = zod.infer<typeof ZodPlaylist>;
export function isPlaylist(data: any): data is Playlist {
try {
ZodPlaylist.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodPlaylist' error=${e}`);
return false;
}
}
export const ZodPlaylistWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
tracks: zod.array(ZodLong).optional(),
});
export type PlaylistWrite = zod.infer<typeof ZodPlaylistWrite>;
export function isPlaylistWrite(data: any): data is PlaylistWrite {
try {
ZodPlaylistWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodPlaylistWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,29 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodUUID} from "./uuid";
import {Zodint, ZodintWrite } from "./int";
export const ZodRestErrorResponse = zod.object({
uuid: ZodUUID.optional(),
name: zod.string(),
message: zod.string(),
time: zod.string(),
status: Zodint,
statusMessage: zod.string(),
});
export type RestErrorResponse = zod.infer<typeof ZodRestErrorResponse>;
export function isRestErrorResponse(data: any): data is RestErrorResponse {
try {
ZodRestErrorResponse.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodRestErrorResponse' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodTimestamp = zod.string().datetime({ precision: 3 });
export type Timestamp = zod.infer<typeof ZodTimestamp>;

View File

@ -0,0 +1,61 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodUUID} from "./uuid";
import {ZodLong} from "./long";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodTrack = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(),
description: zod.string().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).optional(),
genderId: ZodLong.optional(),
albumId: ZodLong.optional(),
track: ZodLong.optional(),
dataId: ZodUUID.optional(),
artists: zod.array(ZodLong),
});
export type Track = zod.infer<typeof ZodTrack>;
export function isTrack(data: any): data is Track {
try {
ZodTrack.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodTrack' error=${e}`);
return false;
}
}
export const ZodTrackWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
genderId: ZodLong.nullable().optional(),
albumId: ZodLong.nullable().optional(),
track: ZodLong.nullable().optional(),
dataId: ZodUUID.nullable().optional(),
artists: zod.array(ZodLong).optional(),
});
export type TrackWrite = zod.infer<typeof ZodTrackWrite>;
export function isTrackWrite(data: any): data is TrackWrite {
try {
ZodTrackWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodTrackWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,37 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodUser, ZodUserWrite } from "./user";
export const ZodUserKarusic = ZodUser.extend({
});
export type UserKarusic = zod.infer<typeof ZodUserKarusic>;
export function isUserKarusic(data: any): data is UserKarusic {
try {
ZodUserKarusic.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserKarusic' error=${e}`);
return false;
}
}
export const ZodUserKarusicWrite = ZodUserWrite.extend({
});
export type UserKarusicWrite = zod.infer<typeof ZodUserKarusicWrite>;
export function isUserKarusicWrite(data: any): data is UserKarusicWrite {
try {
ZodUserKarusicWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserKarusicWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,29 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
import {ZodPartRight} from "./part-right";
export const ZodUserMe = zod.object({
id: ZodLong,
login: zod.string().max(255).optional(),
/**
* Map<EntityName, Map<PartName, Right>>
*/
rights: zod.record(zod.string(), zod.record(zod.string(), ZodPartRight)),
});
export type UserMe = zod.infer<typeof ZodUserMe>;
export function isUserMe(data: any): data is UserMe {
try {
ZodUserMe.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserMe' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,41 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
export const ZodUserOut = zod.object({
id: ZodLong,
login: zod.string().max(255).optional(),
});
export type UserOut = zod.infer<typeof ZodUserOut>;
export function isUserOut(data: any): data is UserOut {
try {
ZodUserOut.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserOut' error=${e}`);
return false;
}
}
export const ZodUserOutWrite = zod.object({
id: ZodLong,
login: zod.string().max(255).nullable().optional(),
});
export type UserOutWrite = zod.infer<typeof ZodUserOutWrite>;
export function isUserOutWrite(data: any): data is UserOutWrite {
try {
ZodUserOutWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserOutWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,57 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodTimestamp} from "./timestamp";
import {ZodUUID} from "./uuid";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodUser = ZodGenericDataSoftDelete.extend({
login: zod.string().max(128).optional(),
lastConnection: ZodTimestamp.optional(),
admin: zod.boolean(),
blocked: zod.boolean(),
removed: zod.boolean(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).optional(),
});
export type User = zod.infer<typeof ZodUser>;
export function isUser(data: any): data is User {
try {
ZodUser.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUser' error=${e}`);
return false;
}
}
export const ZodUserWrite = ZodGenericDataSoftDeleteWrite.extend({
login: zod.string().max(128).nullable().optional(),
lastConnection: ZodTimestamp.nullable().optional(),
admin: zod.boolean(),
blocked: zod.boolean(),
removed: zod.boolean(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
});
export type UserWrite = zod.infer<typeof ZodUserWrite>;
export function isUserWrite(data: any): data is UserWrite {
try {
ZodUserWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodUUID = zod.string().uuid();
export type UUID = zod.infer<typeof ZodUUID>;

View File

@ -0,0 +1,445 @@
/** @file
* @author Edouard DUPIN
* @copyright 2024, Edouard DUPIN, all right reserved
* @license MPL-2
*/
import { RestErrorResponse, isRestErrorResponse } from "./model";
export enum HTTPRequestModel {
DELETE = "DELETE",
GET = "GET",
PATCH = "PATCH",
POST = "POST",
PUT = "PUT",
}
export enum HTTPMimeType {
ALL = "*/*",
CSV = "text/csv",
IMAGE = "image/*",
IMAGE_JPEG = "image/jpeg",
IMAGE_PNG = "image/png",
JSON = "application/json",
MULTIPART = "multipart/form-data",
OCTET_STREAM = "application/octet-stream",
TEXT_PLAIN = "text/plain",
}
export interface RESTConfig {
// base of the server: http(s)://my.server.org/plop/api/
server: string;
// Token to access of the data.
token?: string;
}
export interface RESTModel {
// base of the local API request: "sheep/{id}".
endPoint: string;
// Type of the request.
requestType?: HTTPRequestModel;
// Input type requested.
accept?: HTTPMimeType;
// Content of the local data.
contentType?: HTTPMimeType;
// Mode of the TOKEN in URL or Header (?token:${tokenInUrl})
tokenInUrl?: boolean;
}
export interface ModelResponseHttp {
status: number;
data: any;
}
function isNullOrUndefined(data: any): data is undefined | null {
return data === undefined || data === null;
}
// generic progression callback
export type ProgressCallback = (count: number, total: number) => void;
export interface RESTAbort {
abort?: () => boolean;
}
// Rest generic callback have a basic model to upload and download advancement.
export interface RESTCallbacks {
progressUpload?: ProgressCallback;
progressDownload?: ProgressCallback;
abortHandle?: RESTAbort;
}
export interface RESTRequestType {
restModel: RESTModel;
restConfig: RESTConfig;
data?: any;
params?: object;
queries?: object;
callbacks?: RESTCallbacks;
}
function replaceAll(input, searchValue, replaceValue) {
return input.split(searchValue).join(replaceValue);
}
function removeTrailingSlashes(input: string): string {
if (isNullOrUndefined(input)) {
return "undefined";
}
return input.replace(/\/+$/, "");
}
function removeLeadingSlashes(input: string): string {
if (isNullOrUndefined(input)) {
return "";
}
return input.replace(/^\/+/, "");
}
export function RESTUrl({
restModel,
restConfig,
params,
queries,
}: RESTRequestType): string {
// Create the URL PATH:
let generateUrl = `${removeTrailingSlashes(
restConfig.server
)}/${removeLeadingSlashes(restModel.endPoint)}`;
if (params !== undefined) {
for (let key of Object.keys(params)) {
generateUrl = replaceAll(generateUrl, `{${key}}`, `${params[key]}`);
}
}
if (
queries === undefined &&
(restConfig.token === undefined || restModel.tokenInUrl !== true)
) {
return generateUrl;
}
const searchParams = new URLSearchParams();
if (queries !== undefined) {
for (let key of Object.keys(queries)) {
const value = queries[key];
if (Array.isArray(value)) {
for (const element of value) {
searchParams.append(`${key}`, `${element}`);
}
} else {
searchParams.append(`${key}`, `${value}`);
}
}
}
if (restConfig.token !== undefined && restModel.tokenInUrl === true) {
searchParams.append("Authorization", `Bearer ${restConfig.token}`);
}
return generateUrl + "?" + searchParams.toString();
}
export function fetchProgress(
generateUrl: string,
{
method,
headers,
body,
}: {
method: HTTPRequestModel;
headers: any;
body: any;
},
{ progressUpload, progressDownload, abortHandle }: RESTCallbacks
): Promise<Response> {
const xhr: {
io?: XMLHttpRequest;
} = {
io: new XMLHttpRequest(),
};
return new Promise((resolve, reject) => {
// Stream the upload progress
if (progressUpload) {
xhr.io?.upload.addEventListener("progress", (dataEvent) => {
if (dataEvent.lengthComputable) {
progressUpload(dataEvent.loaded, dataEvent.total);
}
});
}
// Stream the download progress
if (progressDownload) {
xhr.io?.addEventListener("progress", (dataEvent) => {
if (dataEvent.lengthComputable) {
progressDownload(dataEvent.loaded, dataEvent.total);
}
});
}
if (abortHandle) {
abortHandle.abort = () => {
if (xhr.io) {
console.log(`Request abort on the XMLHttpRequest: ${generateUrl}`);
xhr.io.abort();
return true;
}
console.log(
`Request abort (FAIL) on the XMLHttpRequest: ${generateUrl}`
);
return false;
};
}
// Check if we have an internal Fail:
xhr.io?.addEventListener("error", () => {
xhr.io = undefined;
reject(new TypeError("Failed to fetch"));
});
// Capture the end of the stream
xhr.io?.addEventListener("loadend", () => {
if (xhr.io?.readyState !== XMLHttpRequest.DONE) {
return;
}
if (xhr.io?.status === 0) {
//the stream has been aborted
reject(new TypeError("Fetch has been aborted"));
return;
}
// Stream is ended, transform in a generic response:
const response = new Response(xhr.io.response, {
status: xhr.io.status,
statusText: xhr.io.statusText,
});
const headersArray = replaceAll(
xhr.io.getAllResponseHeaders().trim(),
"\r\n",
"\n"
).split("\n");
headersArray.forEach(function (header) {
const firstColonIndex = header.indexOf(":");
if (firstColonIndex !== -1) {
const key = header.substring(0, firstColonIndex).trim();
const value = header.substring(firstColonIndex + 1).trim();
response.headers.set(key, value);
} else {
response.headers.set(header, "");
}
});
xhr.io = undefined;
resolve(response);
});
xhr.io?.open(method, generateUrl, true);
if (!isNullOrUndefined(headers)) {
for (const [key, value] of Object.entries(headers)) {
xhr.io?.setRequestHeader(key, value as string);
}
}
xhr.io?.send(body);
});
}
export function RESTRequest({
restModel,
restConfig,
data,
params,
queries,
callbacks,
}: RESTRequestType): Promise<ModelResponseHttp> {
// Create the URL PATH:
let generateUrl = RESTUrl({ restModel, restConfig, data, params, queries });
let headers: any = {};
if (restConfig.token !== undefined && restModel.tokenInUrl !== true) {
headers["Authorization"] = `Bearer ${restConfig.token}`;
}
if (restModel.accept !== undefined) {
headers["Accept"] = restModel.accept;
}
if (restModel.requestType !== HTTPRequestModel.GET) {
// if Get we have not a content type, the body is empty
if (restModel.contentType !== HTTPMimeType.MULTIPART) {
// special case of multi-part ==> no content type otherwise the browser does not set the ";bundary=--****"
headers["Content-Type"] = restModel.contentType;
}
}
let body = data;
if (restModel.contentType === HTTPMimeType.JSON) {
body = JSON.stringify(data);
} else if (restModel.contentType === HTTPMimeType.MULTIPART) {
const formData = new FormData();
for (const name in data) {
formData.append(name, data[name]);
}
body = formData;
}
return new Promise((resolve, reject) => {
let action: undefined | Promise<Response> = undefined;
if (
isNullOrUndefined(callbacks) ||
(isNullOrUndefined(callbacks.progressDownload) &&
isNullOrUndefined(callbacks.progressUpload) &&
isNullOrUndefined(callbacks.abortHandle))
) {
// No information needed: call the generic fetch interface
action = fetch(generateUrl, {
method: restModel.requestType,
headers,
body,
});
} else {
// need progression information: call old fetch model (XMLHttpRequest) that permit to keep % upload and % download for HTTP1.x
action = fetchProgress(
generateUrl,
{
method: restModel.requestType ?? HTTPRequestModel.GET,
headers,
body,
},
callbacks
);
}
action
.then((response: Response) => {
if (response.status >= 200 && response.status <= 299) {
const contentType = response.headers.get("Content-Type");
if (
!isNullOrUndefined(restModel.accept) &&
restModel.accept !== contentType
) {
reject({
name: "Model accept type incompatible",
time: Date().toString(),
status: 901,
message: `REST Content type are not compatible: ${restModel.accept} != ${contentType}`,
statusMessage: "Fetch error",
error: "rest-tools.ts Wrong type in the message return type",
} as RestErrorResponse);
} else if (contentType === HTTPMimeType.JSON) {
response
.json()
.then((value: any) => {
resolve({ status: response.status, data: value });
})
.catch((reason: Error) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: 902,
message: `REST parse json fail: ${reason}`,
statusMessage: "Fetch parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
});
} else {
resolve({ status: response.status, data: response.body });
}
} else {
// the answer is not correct not a 2XX
// clone the response to keep the raw data if case of error:
response
.clone()
.json()
.then((value: any) => {
if (isRestErrorResponse(value)) {
reject(value);
} else {
response
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: 903,
message: `REST parse error json with wrong type fail. ${dataError}`,
statusMessage: "Fetch parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
});
}
})
.catch((reason: Error) => {
response
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ${dataError} with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT FAIL",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
});
});
}
})
.catch((error: Error) => {
if (isRestErrorResponse(error)) {
reject(error);
} else {
reject({
name: "Request fail",
time: Date(),
status: 999,
message: error,
statusMessage: "Fetch catch error",
error: "rest-tools.ts detect an error in the fetch request",
});
}
});
});
}
export function RESTRequestJson<TYPE>(
request: RESTRequestType,
checker?: (data: any) => data is TYPE
): Promise<TYPE> {
return new Promise((resolve, reject) => {
RESTRequest(request)
.then((value: ModelResponseHttp) => {
if (isNullOrUndefined(checker)) {
console.log(`Have no check of MODEL in API: ${RESTUrl(request)}`);
resolve(value.data);
} else if (checker === undefined || checker(value.data)) {
resolve(value.data);
} else {
reject({
name: "Model check fail",
time: Date().toString(),
status: 950,
error: "REST Fail to verify the data",
statusMessage: "API cast ERROR",
message: "api.ts Check type as fail",
} as RestErrorResponse);
}
})
.catch((reason: RestErrorResponse) => {
reject(reason);
});
});
}
export function RESTRequestVoid(request: RESTRequestType): Promise<void> {
return new Promise((resolve, reject) => {
RESTRequest(request)
.then((value: ModelResponseHttp) => {
resolve();
})
.catch((reason: RestErrorResponse) => {
reject(reason);
});
});
}

View File

@ -0,0 +1,300 @@
import { SyntheticEvent, useEffect, useRef, useState } from 'react';
import {
Box,
Button,
Flex,
IconButton,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
Text,
position,
} from '@chakra-ui/react';
import {
MdFastForward,
MdFastRewind,
MdGraphicEq,
MdNavigateBefore,
MdNavigateNext,
MdOutlinePlayArrow,
MdPause,
MdPlayArrow,
MdStop,
MdTrendingFlat,
} from 'react-icons/md';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificTrack } from '@/service/Track';
import { DataUrlAccess } from '@/utils/data-url-access';
import { useThemeMode } from '@/utils/theme-tools';
export type AudioPlayerProps = {};
const formatTime = (time) => {
if (time && !isNaN(time)) {
const minutes = Math.floor(time / 60);
const formatMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
const seconds = Math.floor(time % 60);
const formatSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${formatMinutes}:${formatSeconds}`;
}
return '00:00';
};
export const AudioPlayer = ({}: AudioPlayerProps) => {
const { mode } = useThemeMode();
const { playTrackList, trackOffset, previous, next } =
useActivePlaylistService();
const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [timeProgress, setTimeProgress] = useState<number>(0);
const [duration, setDuration] = useState<number>(0);
const { dataTrack, updateTrackId } = useSpecificTrack(
trackOffset !== undefined ? playTrackList[trackOffset] : undefined
);
useEffect(() => {
console.log(`detect change of playlist ...`);
updateTrackId(
trackOffset !== undefined ? playTrackList[trackOffset] : undefined
);
}, [playTrackList, trackOffset, updateTrackId]);
const [mediaSource, setMediaSource] = useState<string>('');
useEffect(() => {
setMediaSource(
dataTrack && dataTrack?.dataId
? DataUrlAccess.getUrl(dataTrack?.dataId)
: ''
);
}, [dataTrack, setMediaSource]);
const backColor = mode('back.100', 'back.800');
const configButton = {
borderRadius: 'full',
backgroundColor: '#00000000',
_hover: {
boxShadow: 'outline-over',
bgColor: 'brand.500',
},
};
useEffect(() => {
if (!audioRef || !audioRef.current) {
return;
}
if (isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
}
}, [isPlaying, audioRef]);
const onAudioEnded = () => {
// TODO...
};
const onSeek = (newValue) => {
console.log(`onSeek: ${newValue}`);
if (!audioRef || !audioRef.current) {
return;
}
audioRef.current.currentTime = newValue;
};
const onPlay = () => {
if (!audioRef || !audioRef.current) {
return;
}
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
};
const onStop = () => {
if (!audioRef || !audioRef.current) {
return;
}
if (audioRef.current.currentTime == 0 && audioRef.current.paused) {
// TODO remove curent playing value
} else {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
};
const onNavigatePrevious = () => {
previous();
};
const onFastRewind = () => {
if (!audioRef || !audioRef.current) {
return;
}
audioRef.current.currentTime -= 10;
};
const onFastForward = () => {
if (!audioRef || !audioRef.current) {
return;
}
audioRef.current.currentTime += 10;
};
const onNavigateNext = () => {
next();
};
const onTypePlay = () => {};
/**
* Call when meta-data is updated
*/
function onChangeMetadata(): void {
const seconds = audioRef.current?.duration;
if (seconds !== undefined) {
setDuration(seconds);
}
}
const onTimeUpdate = () => {
if (!audioRef || !audioRef.current) {
return;
}
console.log(`onTimeUpdate ${audioRef.current.currentTime}`);
setTimeProgress(audioRef.current.currentTime);
};
const onDurationChange = (event) => {};
const onChangeStateToPlay = () => {
setIsPlaying(true);
};
const onChangeStateToPause = () => {
setIsPlaying(false);
};
return (
<>
<Flex
position="absolute"
height="150px"
minHeight="150px"
paddingY="5px"
paddingX="10px"
marginX="15px"
bottom={0}
left={0}
right={0}
zIndex={1000}
borderWidth="1px"
borderColor="brand.900"
bgColor={backColor}
borderTopRadius="10px"
direction="column"
>
<Text
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
{dataTrack?.name ?? '???'}
</Text>
<Text
align="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
artist / title album
</Text>
<Box width="full" paddingX="15px">
<Slider
aria-label="slider-ex-4"
defaultValue={0}
value={timeProgress}
min={0}
max={duration}
step={0.1}
onChange={onSeek}
>
<SliderTrack bg="gray.200" height="10px" borderRadius="full">
<SliderFilledTrack bg="brand.600" />
</SliderTrack>
<SliderThumb boxSize={6}>
<Box color="brand.600" as={MdGraphicEq} />
</SliderThumb>
</Slider>
</Box>
<Flex>
<Text
align="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
>
{formatTime(timeProgress)}
</Text>
<Text align="left" fontSize="16px" userSelect="none">
{formatTime(duration)}
</Text>
</Flex>
<Flex gap="5px">
<IconButton
{...configButton}
aria-label={'Play'}
icon={
isPlaying ? <MdPause size="30px" /> : <MdPlayArrow size="30px" />
}
onClick={onPlay}
/>
<IconButton
{...configButton}
aria-label={'Stop'}
icon={<MdStop size="30px" />}
onClick={onStop}
/>
<IconButton
{...configButton}
aria-label={'Previous track'}
icon={<MdNavigateBefore size="30px" />}
onClick={onNavigatePrevious}
marginLeft="auto"
/>
<IconButton
{...configButton}
aria-label={'jump 15sec in past'}
icon={<MdFastRewind size="30px" />}
onClick={onFastRewind}
/>
<IconButton
{...configButton}
aria-label={'jump 15sec in future'}
icon={<MdFastForward size="30px" />}
onClick={onFastForward}
/>
<IconButton
{...configButton}
aria-label={'Next track'}
icon={<MdNavigateNext size="30px" />}
marginRight="auto"
onClick={onNavigateNext}
/>
<IconButton
{...configButton}
aria-label={'continue to the end'}
icon={<MdTrendingFlat size="30px" />}
onClick={onTypePlay}
/>
</Flex>
</Flex>
<audio
src={mediaSource}
ref={audioRef}
//preload={true}
onPlay={onChangeStateToPlay}
onPause={onChangeStateToPause}
onTimeUpdate={onTimeUpdate}
onDurationChange={onDurationChange}
onLoadedMetadata={onChangeMetadata}
autoPlay={true}
onEnded={onAudioEnded}
/>
</>
);
};

View File

@ -0,0 +1,40 @@
import { ReactElement } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Image } from '@chakra-ui/react';
import { DataUrlAccess } from '@/utils/data-url-access';
export type CoversProps = BoxProps & {
data?: string[];
size?: string;
iconEmpty?: ReactElement;
};
export const Covers = ({
data,
iconEmpty,
size = '100px',
...rest
}: CoversProps) => {
if (!data || data.length < 1) {
if (iconEmpty) {
return iconEmpty;
} else {
return (
<Box
width={size}
height={size}
minHeight={size}
minWidth={size}
borderColor="blue"
borderWidth="1px"
margin="auto"
{...rest}
></Box>
);
}
}
const url = DataUrlAccess.getThumbnailUrl(data[0]);
return <Image src={url} boxSize={size} {...rest} />;
};

View File

@ -0,0 +1,50 @@
import React, { ReactNode, useEffect } from 'react';
import { Flex, Image } from '@chakra-ui/react';
import { useLocation } from 'react-router-dom';
import background from '@/assets/images/ikon.svg';
import { TOP_BAR_HEIGHT } from '@/components/TopBar/TopBar';
export type LayoutProps = React.PropsWithChildren<unknown> & {
topBar?: ReactNode;
};
export const PageLayout = ({ children }: LayoutProps) => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return (
<>
<Flex
minH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
maxH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
position="absolute"
top={TOP_BAR_HEIGHT}
bottom={0}
left={0}
right={0}
zIndex={-1}
>
<Image src={background} boxSize="90%" margin="auto" opacity="30%" />
</Flex>
<Flex
direction="column"
overflowX="auto"
overflowY="auto"
minH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
maxH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
position="absolute"
top={TOP_BAR_HEIGHT}
bottom={0}
left={0}
right={0}
>
{children}
</Flex>
</>
);
};

View File

@ -0,0 +1,44 @@
import React, { ReactNode, useEffect } from 'react';
import { Flex, FlexProps } from '@chakra-ui/react';
import { useLocation } from 'react-router-dom';
import { PageLayout } from '@/components/Layout/PageLayout';
import { colors } from '@/theme/foundations/colors';
import { useThemeMode } from '@/utils/theme-tools';
export type LayoutProps = FlexProps & {
children: ReactNode;
};
export const PageLayoutInfoCenter = ({
children,
width = '25%',
...rest
}: LayoutProps) => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
const { mode } = useThemeMode();
return (
<PageLayout>
<Flex
direction="column"
margin="auto"
minWidth={width}
border="back.900"
borderWidth="1px"
borderRadius="8px"
padding="10px"
boxShadow={'0px 0px 16px ' + colors.back[900]}
backgroundColor={mode('#FFFFFF', '#000000')}
{...rest}
>
{children}
</Flex>
</PageLayout>
);
};

View File

@ -0,0 +1,190 @@
import { ReactNode } from 'react';
import {
Button,
Drawer,
DrawerBody,
DrawerContent,
DrawerHeader,
DrawerOverlay,
Flex,
HStack,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Text,
useDisclosure,
} from '@chakra-ui/react';
import {
LuAlignJustify,
LuArrowBigLeft,
LuArrowRightSquare,
LuArrowUpSquare,
LuHelpCircle,
LuHome,
LuLogIn,
LuLogOut,
LuMoon,
LuPlusCircle,
LuSettings,
LuSun,
LuUserCircle,
} from 'react-icons/lu';
import { useNavigate } from 'react-router-dom';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionState } from '@/service/SessionState';
import { colors } from '@/theme/foundations/colors';
import { requestSignIn, requestSignOut, requestSignUp } from '@/utils/sso';
import { useThemeMode } from '@/utils/theme-tools';
export const TOP_BAR_HEIGHT = '50px';
export type TopBarProps = {
children?: ReactNode;
};
export const TopBar = ({ children }: TopBarProps) => {
const { mode, colorMode, toggleColorMode } = useThemeMode();
const buttonProperty = {
variant: '@menu',
height: TOP_BAR_HEIGHT,
};
const { session } = useServiceContext();
const backColor = mode('back.100', 'back.800');
const drawerDisclose = useDisclosure();
const onChangeTheme = () => {
drawerDisclose.onOpen();
};
const navigate = useNavigate();
const onSignIn = (): void => {
requestSignIn();
};
const onSignUp = (): void => {
requestSignUp();
};
const onSignOut = (): void => {
requestSignOut();
};
const onSelectHome = () => {
navigate('/');
};
return (
<Flex
position="absolute"
top={0}
left={0}
right={0}
height={TOP_BAR_HEIGHT}
alignItems="center"
justifyContent="space-between"
backgroundColor={backColor}
gap="2"
px="2"
boxShadow={'0px 2px 4px ' + colors.back[900]}
zIndex={200}
>
<Button {...buttonProperty} onClick={onChangeTheme} marginRight="auto">
<LuAlignJustify />
<Text paddingLeft="3px" fontWeight="bold">
Menu
</Text>
</Button>
{children}
<Text
fontSize="25px"
fontWeight="bold"
textTransform="uppercase"
marginRight="auto"
userSelect="none"
>
Karusic
</Text>
{session?.state !== SessionState.CONNECTED && (
<>
<Button {...buttonProperty} onClick={onSignIn}>
<LuLogIn />
<Text paddingLeft="3px" fontWeight="bold">
Sign-in
</Text>
</Button>
<Button {...buttonProperty} onClick={onSignUp} disabled={true}>
<LuPlusCircle />
<Text paddingLeft="3px" fontWeight="bold">
Sign-up
</Text>
</Button>
</>
)}
{session?.state === SessionState.CONNECTED && (
<Menu>
<MenuButton
as={IconButton}
aria-label="Options"
icon={<LuUserCircle />}
{...buttonProperty}
width={TOP_BAR_HEIGHT}
/>
<MenuList>
<MenuItem _hover={{}} color={mode('brand.800', 'brand.200')}>
Sign in as {session?.login ?? 'Fail'}
</MenuItem>
<MenuItem icon={<LuArrowUpSquare />}>Add Media</MenuItem>
<MenuItem icon={<LuSettings />}>Settings</MenuItem>
<MenuItem icon={<LuHelpCircle />}>Help</MenuItem>
<MenuItem icon={<LuLogOut onClick={onSignOut} />}>
Sign-out
</MenuItem>
{colorMode === 'light' ? (
<MenuItem icon={<LuMoon />} onClick={toggleColorMode}>
Set dark mode
</MenuItem>
) : (
<MenuItem icon={<LuSun />} onClick={toggleColorMode}>
Set light mode
</MenuItem>
)}
</MenuList>
</Menu>
)}
<Drawer
placement="left"
onClose={drawerDisclose.onClose}
isOpen={drawerDisclose.isOpen}
>
<DrawerOverlay />
<DrawerContent>
<DrawerHeader
paddingY="auto"
as="button"
onClick={drawerDisclose.onClose}
boxShadow={'0px 2px 4px ' + colors.back[900]}
backgroundColor={backColor}
color={mode('brand.900', 'brand.50')}
textTransform="uppercase"
>
<HStack height={TOP_BAR_HEIGHT}>
<LuArrowBigLeft />
<Text as="span" paddingLeft="3px">
Karusic
</Text>
</HStack>
</DrawerHeader>
<DrawerBody>
<Button {...buttonProperty} onClick={onSelectHome} width="fill">
<LuHome />
<Text paddingLeft="3px" fontWeight="bold">
Home
</Text>
</Button>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</DrawerBody>
</DrawerContent>
</Drawer>
</Flex>
);
};

View File

@ -0,0 +1,3 @@
export * from './Icons';

View File

@ -0,0 +1,28 @@
import dayjs from 'dayjs';
import 'dayjs/locale/fr';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import dayOfYear from 'dayjs/plugin/dayOfYear';
import duration from 'dayjs/plugin/duration';
import isBetween from 'dayjs/plugin/isBetween';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isToday from 'dayjs/plugin/isToday';
import isTomorrow from 'dayjs/plugin/isTomorrow';
import isYesterday from 'dayjs/plugin/isYesterday';
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
import relativeTime from 'dayjs/plugin/relativeTime';
import weekOfYear from 'dayjs/plugin/weekOfYear';
dayjs.locale('fr');
dayjs.extend(relativeTime);
dayjs.extend(customParseFormat);
dayjs.extend(weekOfYear);
dayjs.extend(isSameOrAfter);
dayjs.extend(isToday);
dayjs.extend(isTomorrow);
dayjs.extend(isYesterday);
dayjs.extend(dayOfYear);
dayjs.extend(isBetween);
dayjs.extend(advancedFormat);
dayjs.extend(quarterOfYear);
dayjs.extend(duration);

View File

@ -0,0 +1,2 @@
import './axios';
import './dayjs';

View File

@ -0,0 +1,2 @@
export const DATE_FORMAT = 'YYYY-MM-DD';
export const DATE_FORMAT_FULL = 'dddd DD MMMM HH:mm';

View File

@ -0,0 +1 @@
export * from './date'

107
front2/src/environment.ts Normal file
View File

@ -0,0 +1,107 @@
export interface Environment {
production: boolean;
applName: string;
defaultServer: string;
server: {
[key: string]: string;
};
ssoSite: string;
ssoSignIn: string;
ssoSignUp: string;
ssoSignOut: string;
tokenStoredInPermanentStorage: boolean;
replaceDataToRealServer?: boolean;
}
const serverSSOAddress = 'http://atria-soft.org';
const environment_back_prod: Environment = {
production: false,
// URL of development API
applName: 'karusic',
defaultServer: 'karusic',
server: {
karusic: `${serverSSOAddress}/karusic/api`,
karso: `${serverSSOAddress}/karso/api`,
},
ssoSite: `${serverSSOAddress}/karso/`,
ssoSignIn: `${serverSSOAddress}/karso/signin/karusic-dev/`,
ssoSignUp: `${serverSSOAddress}/karso/signup/karusic-dev/`,
ssoSignOut: `${serverSSOAddress}/karso/signout/karusic-dev/`,
tokenStoredInPermanentStorage: false,
};
const environment_local: Environment = {
production: false,
// URL of development API
applName: 'karusic',
defaultServer: 'karusic',
server: {
karusic: 'http://localhost:19080/karusic/api',
karso: `${serverSSOAddress}/karso/api`,
},
ssoSite: `${serverSSOAddress}/karso/`,
ssoSignIn: `${serverSSOAddress}/karso/signin/karusic-dev/`,
ssoSignUp: `${serverSSOAddress}/karso/signup/karusic-dev/`,
ssoSignOut: `${serverSSOAddress}/karso/signout/karusic-dev/`,
tokenStoredInPermanentStorage: false,
replaceDataToRealServer: true,
};
const environment_full_local: Environment = {
production: false,
// URL of development API
applName: 'karusic',
defaultServer: 'karusic',
server: {
karusic: 'http://localhost:19080/karusic/api',
karso: 'http://localhost:15080/karso/api',
},
ssoSite: `${serverSSOAddress}/karso/`,
ssoSignIn: 'http://localhost:4200/signin/karusic-dev/',
ssoSignUp: 'http://localhost:4200/signup/karusic-dev/',
ssoSignOut: 'http://localhost:4200/signout/karusic-dev/',
tokenStoredInPermanentStorage: false,
};
const environment_hybrid: Environment = {
production: false,
// URL of development API
applName: 'karusic',
defaultServer: 'karusic',
server: {
karusic: `${serverSSOAddress}/karusic/api`,
karso: `${serverSSOAddress}/karso/api`,
},
ssoSite: `${serverSSOAddress}/karso/`,
ssoSignIn: 'http://localhost:4200/signin/karusic-dev/',
ssoSignUp: 'http://localhost:4200/signup/karusic-dev/',
ssoSignOut: 'http://localhost:4200/signout/karusic-dev/',
tokenStoredInPermanentStorage: false,
};
export const environment = environment_local;
/**
* Check if the current environment is for development
* @returns true if development is active.
*/
export const isDevelopmentEnvironment = () => {
return import.meta.env.MODE === 'development';
};
/**
* get the current REST api URL. Depend on the VITE_API_BASE_URL env variable.
* @returns The URL with http(s)://***
*/
export const getApiUrl = () => {
const baseUrl: string | undefined = import.meta.env.VITE_API_BASE_URL;
if (baseUrl === undefined || baseUrl === null) {
//return `${window.location.protocol}//${window.location.host}/api`;
return environment.server.karusic;
}
if (baseUrl.startsWith('http')) {
return baseUrl;
}
return `${window.location.protocol}//${window.location.host}/${baseUrl}`;
};

View File

@ -0,0 +1,132 @@
import {
Box,
Button,
Center,
Heading,
Stack,
Text,
useTheme,
} from '@chakra-ui/react';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { environment } from '@/environment';
const Illustration = ({ colorScheme = 'gray', ...rest }) => {
const theme = useTheme();
const color = theme?.colors?.[colorScheme] ?? {};
return (
<Box
as="svg"
width={400}
height={300}
maxW="full"
viewBox="0 0 400 300"
fill="none"
{...rest}
>
<path
// Left Hand
d="M65.013 104.416s-12.773-.562-13.719 11.938c-.946 12.5 16.13 8.397 13.719-11.938z"
fill={color['300']}
/>
<path
// Left Arm
d="M182.326 67.705s-35.463-20.529-67.804-13.535c-32.342 6.993-60.624 52.94-60.624 52.94l11.499 6.837s49.74-51.775 83.275-21.444c33.535 30.331 33.654-24.798 33.654-24.798z"
fill={color['800']}
/>
<path
// Search Zone
d="M334.098 220.092a14.333 14.333 0 01-9.838-7.37v-.106l-50.465-96.17-27.774 19.677 19.642 61.796a10.575 10.575 0 01-5.617 12.799 10.563 10.563 0 01-4.945.97 1037.507 1037.507 0 00-47.278-1.067c-85.178 0-154.23 9.93-154.23 22.184 0 12.255 69.052 22.195 154.23 22.195C293.001 255 362 245.07 362 232.837c0-4.756-10.328-9.14-27.902-12.745z"
fill={color['200']}
/>
<path
// Foots
d="M173.611 225.563s1.578 5.333 6.256 5.962c4.679.63 5.66 5.333 1.365 6.293-4.296.96-14.921-5.066-14.921-5.066l.671-6.773 6.629-.416zM82.518 224.657s-5.414 1.173-6.395 5.791c-.98 4.618-5.734 5.237-6.395.875-.66-4.362 6.193-14.484 6.193-14.484l6.693 1.205-.096 6.613z"
fill={color['900']}
/>
<path
// Left Leg
d="M83.5 143s-5.245 25.322 12.713 35.305c17.959 9.983 74.606-7.988 65.856 48.592h12.64s16.338-46.928-26.048-63.609l-12.864-14.601L83.5 143z"
fill={color['600']}
/>
<path
// Magnifying Glass Shadow
d="M257.632 128.216l-4.299-4.112-26.891 28.16 4.299 4.111 26.891-28.159z"
fill={color['700']}
/>
<path
// Magnifying Glass Handle
d="M255.537 126.2l-4.299-4.112-26.891 28.16 4.299 4.111 26.891-28.159z"
fill={color['500']}
/>
<path
// Magnifying Glass Shadow 2
d="M267.233 131.381c6.913-7.239 8.606-16.849 3.78-21.464-4.826-4.615-14.342-2.487-21.256 4.752-6.913 7.24-8.605 16.85-3.779 21.465 4.825 4.615 14.342 2.487 21.255-4.753z"
fill={color['700']}
/>
<path
// Magnifying Glass Ring
d="M265.133 129.382c6.914-7.24 8.606-16.849 3.78-21.464-4.825-4.615-14.342-2.487-21.255 4.752-6.913 7.24-8.606 16.849-3.78 21.464 4.826 4.615 14.342 2.487 21.255-4.752z"
fill={color['500']}
/>
<path
// Magnifying Glass
d="M262.167 126.545c4.566-4.782 5.685-11.13 2.497-14.178-3.187-3.048-9.473-1.642-14.04 3.14-4.567 4.782-5.685 11.13-2.498 14.178 3.188 3.048 9.474 1.642 14.041-3.14z"
fill={color['50']}
/>
<path
// Head
d="M217.261 74.257c1.932-2.325 3.256-4.248 3.256-4.248 3.133-4.106-.267-11.743-6.096-12.084a7.606 7.606 0 00-7.759 4.095l-.966 2.572-19.039 7.678 2.664 15.443 14.063-11.483c.418 1.245 1.052 2.35 1.7 3.26a4.269 4.269 0 004.448 1.638 4.267 4.267 0 001.51-.7 25.197 25.197 0 002.341-1.98l1.613.893a1.364 1.364 0 002.014-1.067l.256-4.02-.005.003z"
fill={color['300']}
/>
<path
// Body
d="M192.199 93.056l-1.791-10.42a25.45 25.45 0 00-15.251-19.325 45.122 45.122 0 00-10.754-2.827c-4.732-.66-11.361 0-18.779 1.952-31.953 8.394-55.4 35.484-60.25 68.141L83 145l52.797 3.687s-2.664-14.931 15.987-14.931 42.269.192 40.415-40.7z"
fill={color['700']}
/>
<path
// Right Arm
d="M169.945 95.488c4.977-16.617-14.324-30.055-28.084-19.496-11.51 8.82-24.513 24.701-25.024 51.503-.885 48.368 45.413 39.718 108.071 21.192l-1.993-14.089s-75.032 14.196-64.843-10.207c4.263-10.206 9.23-20.061 11.873-28.903z"
fill={color['800']}
/>
<path
// Right Leg
d="M143.609 163.406s1.545 53.487-60.995 63.491l-2.42-11.338s50.092-12.425 17.735-45.712l45.68-6.441z"
fill={color['600']}
/>
307s17
<path
// Right Hand
d="M223.298 137.307s17.244-1.824 18.758 4.917c1.513 6.741-1.791 10.313-17.309 5.333l-1.449-10.25z"
fill={color['300']}
/>
<path
// Hair
d="M218.673 68.942a34.11 34.11 0 01-5.649-5.44 7.094 7.094 0 01-4.934 5.994 5.752 5.752 0 01-6.821-2.655l7.034-8.319a8.644 8.644 0 018.27-3.2c1.33.23 2.643.543 3.933.939 3.197 1.066 5.425 5.567 8.942 5.717a2.044 2.044 0 011.946 2.1c-.01.354-.111.7-.294 1.003-1.727 2.87-5.499 6.517-10.114 5.056a7.933 7.933 0 01-2.313-1.195z"
fill={color['800']}
/>
</Box>
);
};
export const Error404 = () => {
return (
<>
<TopBar />
<PageLayoutInfoCenter>
<Illustration />
<Box textAlign={{ base: 'center', md: 'left' }}>
<Heading>Erreur 404</Heading>
<Text color="gray.600">
Cette page n'existe plus ou l'URL a changé
</Text>
<Button as="a" variant="link" href={`/${environment.applName}`}>
Retour à l'accueil
</Button>
</Box>
</PageLayoutInfoCenter>
</>
);
};

View File

@ -0,0 +1,51 @@
import React, { FC } from 'react';
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
Button,
Collapse,
useDisclosure,
} from '@chakra-ui/react';
import {
FallbackProps,
ErrorBoundary as ReactErrorBoundary,
} from 'react-error-boundary';
import { LuChevronDown, LuChevronUp } from 'react-icons/lu';
const ErrorFallback = ({ error }: FallbackProps) => {
const { isOpen, onToggle } = useDisclosure();
return (
<Box p="4" m="auto">
<Alert status="error" borderRadius="md">
<AlertIcon />
<Box flex="1">
<AlertTitle>An unexpected error has occurred.</AlertTitle>
<AlertDescription display="block" lineHeight="1.4">
<Button
variant="link"
color="red.800"
size="sm"
rightIcon={isOpen ? <LuChevronUp /> : <LuChevronDown />}
onClick={onToggle}
>
Show details
</Button>
<Collapse in={isOpen} animateOpacity>
<Box mt={4} fontFamily="monospace">
{error.message}
</Box>
</Collapse>
</AlertDescription>
</Box>
</Alert>
</Box>
);
};
export const ErrorBoundary: FC<React.PropsWithChildren<unknown>> = (props) => {
return <ReactErrorBoundary FallbackComponent={ErrorFallback} {...props} />;
};

View File

@ -0,0 +1 @@
export * from './Error404';

View File

@ -0,0 +1,14 @@
import { createIcon } from '@chakra-ui/react';
export const DoubleArrowIcon = createIcon({
displayName: 'DoubleArrowIcon',
viewBox: '0 0 24 24',
path: (
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.293 12.207a1 1 0 0 1 0-1.414l6.364-6.364A1 1 0 0 1 9.07 5.843L4.414 10.5h15.172l-4.657-4.657a1 1 0 0 1 1.414-1.414l6.364 6.364a1 1 0 0 1 0 1.414l-6.364 6.364a1 1 0 0 1-1.414-1.414l4.657-4.657H4.414l4.657 4.657a1 1 0 1 1-1.414 1.414l-6.364-6.364Z"
fill="currentColor"
/>
),
});

View File

@ -0,0 +1 @@
export * from './DoubleArrowIcon';

16
front2/src/main.tsx Normal file
View File

@ -0,0 +1,16 @@
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import App from '@/App';
// Render the app
const rootElement = document.getElementById('root');
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
}

35
front2/src/scene/App.tsx Normal file
View File

@ -0,0 +1,35 @@
import { createBrowserHistory } from 'history';
import {
unstable_HistoryRouter as HistoryRouter,
Route,
Routes,
} from 'react-router-dom';
import { AudioPlayer } from '@/components/AudioPlayer';
import { Error404 } from '@/errors';
import { ErrorBoundary } from '@/errors/ErrorBoundary';
import { ArtistRoutes } from '@/scene/artist/ArtistRoutes';
import { HomePage } from '@/scene/home/HomePage';
import { SSORoutes } from '@/scene/sso/SSORoutes';
import { ServiceContextProvider } from '@/service/ServiceContext';
export const App = () => {
return (
<ServiceContextProvider>
<ErrorBoundary>
<HistoryRouter
history={createBrowserHistory({ window })}
basename="/karusic"
>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="artist/*" element={<ArtistRoutes />} />
<Route path="sso/*" element={<SSORoutes />} />
<Route path="*" element={<Error404 />} />
</Routes>
</HistoryRouter>
</ErrorBoundary>
<AudioPlayer />
</ServiceContextProvider>
);
};

View File

@ -0,0 +1,180 @@
import { Box, Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { LuDisc3, LuFileAudio, LuMusic2, LuUser } from 'react-icons/lu';
import { useNavigate, useParams } from 'react-router-dom';
import { Album, Artist, Track } from '@/back-api';
import { Covers } from '@/components/Cover';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificAlbum } from '@/service/Album';
import { useArtistService, useSpecificArtist } from '@/service/Artist';
import { useAlbumIdsOfAnArtist, useTracksOfAnAlbum } from '@/service/Track';
import { useThemeMode } from '@/utils/theme-tools';
export type DisplayTrackProps = {
track: Track;
};
export const DisplayTrack = ({ track }: DisplayTrackProps) => {
return (
<Flex direction="row" width="full" height="full">
<Covers
data={track?.covers}
size="50"
height="full"
iconEmpty={<LuMusic2 size="50" height="full" />}
/>
<Flex
direction="column"
width="full"
height="full"
paddingLeft="5px"
overflowX="hidden"
>
<Text
as="span"
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={[1, 2]}
marginY="auto"
>
[{track.track}] {track.name}
</Text>
</Flex>
</Flex>
);
};
export const EmptyEnd = () => {
return (
<Box
width="full"
height="25%"
minHeight="25%"
borderWidth="1px"
borderColor="red"
></Box>
);
};
export const ArtistAlbumDetailPage = () => {
const { artistId, albumId } = useParams();
const artistIdInt = artistId ? parseInt(artistId, 10) : undefined;
const albumIdInt = albumId ? parseInt(albumId, 10) : undefined;
const { mode } = useThemeMode();
const { playInList } = useActivePlaylistService();
const { dataArtist } = useSpecificArtist(artistIdInt);
const { dataAlbum } = useSpecificAlbum(albumIdInt);
const { tracksOnAnAlbum } = useTracksOfAnAlbum(albumIdInt);
const onSelectItem = (trackId: number) => {
//navigate(`/artist/${artistIdInt}/album/${albumId}`);
let currentPlay = 0;
const listTrackId: number[] = [];
for (let iii = 0; iii < tracksOnAnAlbum.length; iii++) {
listTrackId.push(tracksOnAnAlbum[iii].id);
if (tracksOnAnAlbum[iii].id === trackId) {
currentPlay = iii;
}
}
playInList(currentPlay, listTrackId);
};
console.log(`dataAlbum = ${JSON.stringify(dataAlbum, null, 2)}`);
if (!dataAlbum) {
<>
<TopBar />
<PageLayoutInfoCenter>
Fail to load artist id: {artistId}
</PageLayoutInfoCenter>
</>;
}
return (
<>
<TopBar />
<PageLayout>
<Flex
direction="row"
width="80%"
marginX="auto"
padding="10px"
gap="10px"
>
<Covers
data={dataArtist?.covers}
iconEmpty={<LuUser size="100" height="full" />}
/>
<Flex direction="column" width="80%" marginRight="auto">
<Text fontSize="24px" fontWeight="bold">
{dataArtist?.name}
</Text>
{dataArtist?.description && (
<Text>Description: {dataArtist?.description}</Text>
)}
{dataArtist?.firstName && (
<Text>first name: {dataArtist?.firstName}</Text>
)}
{dataArtist?.surname && <Text>surname: {dataArtist?.surname}</Text>}
{dataArtist?.birth && <Text>birth: {dataArtist?.birth}</Text>}
</Flex>
</Flex>
<Flex
direction="row"
width="80%"
marginX="auto"
padding="10px"
gap="10px"
>
<Covers
data={dataAlbum?.covers}
iconEmpty={<LuDisc3 size="100" height="full" />}
/>
<Flex direction="column" width="80%" marginRight="auto">
<Text fontSize="24px" fontWeight="bold">
{dataAlbum?.name}
</Text>
{dataAlbum?.description && (
<Text>Description: {dataAlbum?.description}</Text>
)}
{dataAlbum?.publication && (
<Text>first name: {dataAlbum?.publication}</Text>
)}
</Flex>
</Flex>
<Flex
direction="column"
gap="20px"
marginX="auto"
padding="20px"
width="80%"
>
{tracksOnAnAlbum?.map((data) => (
<Box
minWidth="100%"
height="60px"
border="1px"
borderColor="brand.900"
backgroundColor={mode('#FFFFFF88', '#00000088')}
key={data.id}
padding="5px"
as="button"
_hover={{
boxShadow: 'outline-over',
bgColor: mode('#FFFFFFF7', '#000000F7'),
}}
onClick={() => onSelectItem(data.id)}
>
<DisplayTrack track={data} />
</Box>
))}
<EmptyEnd />
</Flex>
</PageLayout>
</>
);
};

View File

@ -0,0 +1,126 @@
import { Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { LuDisc3, LuUser } from 'react-icons/lu';
import { useNavigate, useParams } from 'react-router-dom';
import { Album, Artist } from '@/back-api';
import { Covers } from '@/components/Cover';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { useSpecificAlbum } from '@/service/Album';
import { useArtistService, useSpecificArtist } from '@/service/Artist';
import { useAlbumIdsOfAnArtist } from '@/service/Track';
import { useThemeMode } from '@/utils/theme-tools';
export type DisplayAlbumProps = {
id: number;
};
export const DisplayAlbum = ({ id }: DisplayAlbumProps) => {
const { dataAlbum } = useSpecificAlbum(id);
return (
<Flex direction="row" width="full" height="full">
<Covers
data={dataAlbum?.covers}
size="100"
height="full"
iconEmpty={<LuDisc3 size="100" height="full" />}
/>
<Flex
direction="column"
width="150px"
maxWidth="150px"
height="full"
paddingLeft="5px"
overflowX="hidden"
>
<Text
as="span"
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={[1, 2]}
>
{dataAlbum?.name}
</Text>
</Flex>
</Flex>
);
};
export const ArtistDetailPage = () => {
const { artistId } = useParams();
const artistIdInt = artistId ? parseInt(artistId, 10) : undefined;
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (albumId: number) => {
navigate(`/artist/${artistIdInt}/album/${albumId}`);
};
const { dataArtist } = useSpecificArtist(artistIdInt);
const { albumIdsOfAnArtist } = useAlbumIdsOfAnArtist(artistIdInt);
if (!dataArtist) {
<>
<TopBar />
<PageLayoutInfoCenter>
Fail to load artist id: {artistId}
</PageLayoutInfoCenter>
</>;
}
return (
<>
<TopBar />
<PageLayout>
<Flex
direction="row"
width="80%"
marginX="auto"
padding="10px"
gap="10px"
>
<Covers
data={dataArtist?.covers}
iconEmpty={<LuUser size="100" height="full" />}
/>
<Flex direction="column" width="80%" marginRight="auto">
<Text fontSize="24px" fontWeight="bold">
{dataArtist?.name}
</Text>
{dataArtist?.description && (
<Text>Description: {dataArtist?.description}</Text>
)}
{dataArtist?.firstName && (
<Text>first name: {dataArtist?.firstName}</Text>
)}
{dataArtist?.surname && <Text>surname: {dataArtist?.surname}</Text>}
{dataArtist?.birth && <Text>birth: {dataArtist?.birth}</Text>}
</Flex>
</Flex>
<Wrap spacing="20px" marginX="auto" padding="20px" justify="center">
{albumIdsOfAnArtist?.map((data) => (
<WrapItem
width="270px"
height="120px"
border="1px"
borderColor="brand.900"
backgroundColor={mode('#FFFFFF88', '#00000088')}
key={data}
padding="5px"
as="button"
_hover={{
boxShadow: 'outline-over',
bgColor: mode('#FFFFFFF7', '#000000F7'),
}}
onClick={() => onSelectItem(data)}
>
<DisplayAlbum id={data} key={data} />
</WrapItem>
))}
</Wrap>
</PageLayout>
</>
);
};

View File

@ -0,0 +1,21 @@
import { Navigate, Route, Routes } from 'react-router-dom';
import { Error404 } from '@/errors';
import { ArtistAlbumDetailPage } from '@/scene/artist/ArtistAlbumDetailPage';
import { ArtistDetailPage } from '@/scene/artist/ArtistDetailPage';
import { ArtistsPage } from '@/scene/artist/ArtistsPage';
export const ArtistRoutes = () => {
return (
<Routes>
<Route path="/" element={<Navigate to="all" replace />} />
<Route path="all" element={<ArtistsPage />} />
<Route path=":artistId" element={<ArtistDetailPage />} />
<Route
path=":artistId/album/:albumId"
element={<ArtistAlbumDetailPage />}
/>
<Route path="*" element={<Error404 />} />
</Routes>
);
};

View File

@ -0,0 +1,76 @@
import { Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { LuUser } from 'react-icons/lu';
import { useNavigate } from 'react-router-dom';
import { Artist } from '@/back-api';
import { Covers } from '@/components/Cover';
import { PageLayout } from '@/components/Layout/PageLayout';
import { TopBar } from '@/components/TopBar/TopBar';
import { useArtistService } from '@/service/Artist';
import { useThemeMode } from '@/utils/theme-tools';
export const ArtistsPage = () => {
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (data: Artist) => {
navigate(`/artist/${data.id}/`);
};
const { store } = useArtistService();
return (
<>
<TopBar />
<PageLayout>
<Text>All Artists:</Text>
<Wrap spacing="20px" marginX="auto" padding="20px">
{store.data?.map((data) => (
<WrapItem
width="270px"
height="120px"
border="1px"
borderColor="brand.900"
backgroundColor={mode('#FFFFFF88', '#00000088')}
key={data.id}
padding="5px"
as="button"
_hover={{
boxShadow: 'outline-over',
bgColor: mode('#FFFFFFF7', '#000000F7'),
}}
onClick={() => onSelectItem(data)}
>
<Flex direction="row" width="full" height="full">
<Covers
data={data.covers}
size="100"
height="full"
iconEmpty={<LuUser size="100" height="full" />}
/>
<Flex
direction="column"
width="150px"
maxWidth="150px"
height="full"
paddingLeft="5px"
overflowX="hidden"
>
<Text
as="span"
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={[1, 2]}
>
{data.name}
</Text>
</Flex>
</Flex>
</WrapItem>
))}
</Wrap>
</PageLayout>
</>
);
};

View File

@ -0,0 +1,96 @@
import { ReactElement } from 'react';
import { Center, Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { LuCrown, LuDisc3, LuEar, LuFileAudio, LuUser } from 'react-icons/lu';
import { useNavigate } from 'react-router-dom';
import { PageLayout } from '@/components/Layout/PageLayout';
import { TopBar } from '@/components/TopBar/TopBar';
import { useThemeMode } from '@/utils/theme-tools';
type HomeListType = {
id: number;
name: string;
icon: ReactElement;
to: string;
};
const homeList: HomeListType[] = [
{
id: 1,
name: 'Genders',
icon: <LuCrown size="60%" height="full" />,
to: 'gender',
},
{
id: 2,
name: 'Artists',
icon: <LuUser size="60%" height="full" />,
to: 'artist',
},
{
id: 3,
name: 'Albums',
icon: <LuDisc3 size="60%" height="full" />,
to: 'album',
},
{
id: 4,
name: 'Tracks',
icon: <LuFileAudio size="60%" height="full" />,
to: 'track',
},
{
id: 5,
name: 'Playlists',
icon: <LuEar size="60%" height="full" />,
to: 'playlists',
},
];
export const HomePage = () => {
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (data: HomeListType) => {
navigate(data.to);
};
return (
<>
<TopBar />
<PageLayout>
<Wrap spacing="20px" marginX="auto" padding="20px">
{homeList.map((data) => (
<WrapItem
width="200px"
height="190px"
border="1px"
borderColor="brand.900"
backgroundColor={mode('#FFFFFF88', '#00000088')}
key={data.id}
padding="5px"
as="button"
_hover={{
boxShadow: 'outline-over',
bgColor: mode('#FFFFFFF7', '#000000F7'),
}}
onClick={() => onSelectItem(data)}
>
<Flex direction="column" width="full" height="full">
<Center height="full">{data.icon}</Center>
<Center>
<Text
fontSize="25px"
fontWeight="bold"
textTransform="uppercase"
userSelect="none"
>
{data.name}
</Text>
</Center>
</Flex>
</WrapItem>
))}
</Wrap>
</PageLayout>
</>
);
};

View File

@ -0,0 +1,85 @@
import { useEffect } from 'react';
import { Center, Heading, Image, Text } from '@chakra-ui/react';
import { useNavigate, useParams } from 'react-router-dom';
import avatar_generic from '@/assets/images/avatar_generic.svg';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { isDevelopmentEnvironment } from '@/environment';
import { SessionState } from '@/service/SessionState';
import { useSessionService } from '@/service/session';
import { b64_to_utf8 } from '@/utils/sso';
export const SSOPage = () => {
const { data, keepConnected, token } = useParams();
console.log(`- data: ${data}`);
console.log(`- keepConnected: ${keepConnected}`);
console.log(`- token: ${token}`);
const navigate = useNavigate();
const { state, setToken, login, clearToken } = useSessionService();
useEffect(() => {
if (token) {
setToken(token);
} else {
clearToken();
}
}, [token, setToken, clearToken]);
const delay = isDevelopmentEnvironment() ? 20000 : 2000;
useEffect(() => {
if (state === SessionState.CONNECTED) {
const destination = data ? b64_to_utf8(data) : '/';
console.log(`program redirect to: ${destination} (${delay}ms)`);
setTimeout(() => {
navigate(`/${destination}`);
}, delay);
}
}, [state]);
/*
const [searchParams] = useSearchParams();
console.log(`data: ${searchParams.get('data')}`);
console.log(`keepConnected: ${searchParams.get('keepConnected')}`);
console.log(`token: ${searchParams.get('token')}`);
const dataFromParam = useGetCreateActionParams();
console.log(`data group: ${JSON.stringify(dataFromParam, null, 2)}`);
*/
return (
<>
<TopBar />
<PageLayoutInfoCenter width="35%" gap="15px">
<Center w="full">
<Heading size="xl">LOGIN (after SSO) </Heading>
</Center>
<Center w="full">
<Image src={avatar_generic} boxSize="150px" borderRadius="full" />
</Center>
{token === '__CANCEL__' && (
<Text>
<b>ERROR: </b> Request cancel of connection !
</Text>
)}
{token === '__FAIL__' && (
<Text>
<b>ERROR: </b> Connection FAIL !
</Text>
)}
{token === '__LOGOUT__' && (
<Text>
<b>Dis-connected: </b> Redirect soon!{' '}
</Text>
)}
{!['__LOGOUT__', '__FAIL__', '__CANCEL__'].includes(token ?? '') && (
<>
<Text>
<b>Connected: </b> Redirect soon!
</Text>
<Text>
<b>Welcome back: </b> {login}
</Text>
</>
)}
</PageLayoutInfoCenter>
</>
);
};

View File

@ -0,0 +1,13 @@
import { Route, Routes } from 'react-router-dom';
import { Error404 } from '@/errors';
import { SSOPage } from '@/scene/sso/SSOPage';
export const SSORoutes = () => {
return (
<Routes>
<Route path=":data/:keepConnected/:token" element={<SSOPage />} />
<Route path="*" element={<Error404 />} />
</Routes>
);
};

View File

@ -0,0 +1,125 @@
import { useCallback, useState } from 'react';
import { useServiceContext } from '@/service/ServiceContext';
export type PlaylistElement = {
trackId: number;
};
export type ActivePlaylistServiceProps = {
playTrackList: number[];
trackOffset?: number;
setNewPlaylist: (listIds: number[]) => void;
setNewPlaylistShuffle: (listIds: number[]) => void;
playInList: (id: number, listIds: number[]) => void;
play: (id: number) => void;
previous: () => void;
next: () => void;
};
export const useActivePlaylistService = (): ActivePlaylistServiceProps => {
const { activePlaylist } = useServiceContext();
return activePlaylist;
};
export function localShuffle<T>(array: T[]): T[] {
let currentIndex = array.length,
randomIndex;
// While there remain elements to shuffle.
while (currentIndex != 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex],
];
}
return array;
}
export const useActivePlaylistServiceWrapped =
(): ActivePlaylistServiceProps => {
const [playTrackList, setPlayTrackList] = useState<number[]>([]);
const [trackOffset, setTrackOffset] = useState<number | undefined>();
const clear = useCallback(() => {
setPlayTrackList([]);
setTrackOffset(undefined);
}, [setPlayTrackList, setTrackOffset]);
const play = useCallback(
(id: number) => {
setPlayTrackList([id]);
setTrackOffset(0);
},
[setPlayTrackList, setTrackOffset]
);
const playInList = useCallback(
(id: number, listIds: number[]) => {
console.log(`Request paly in list: ${id} in ${listIds}`);
setPlayTrackList(listIds);
setTrackOffset(id);
},
[setPlayTrackList, setTrackOffset]
);
const setNewPlaylist = useCallback(
(listIds: number[]) => {
if (listIds.length == 0) {
clear();
return;
}
setPlayTrackList(listIds);
setTrackOffset(0);
},
[setPlayTrackList, setTrackOffset, clear]
);
const setNewPlaylistShuffle = useCallback(
(listIds: number[]) => {
if (listIds.length == 0) {
clear();
return;
}
const newList = localShuffle(listIds);
setPlayTrackList(newList);
setTrackOffset(0);
},
[setPlayTrackList, setTrackOffset, clear]
);
const previous = useCallback(() => {
setTrackOffset((previous) => {
if (previous === undefined) {
return previous;
}
if (previous === 0) {
return previous;
}
return previous - 1;
});
}, [setTrackOffset]);
const next = useCallback(() => {
setTrackOffset((previous) => {
if (previous === undefined || playTrackList.length === 0) {
return previous;
}
if (previous >= playTrackList.length - 1) {
return previous;
}
return previous + 1;
});
}, [playTrackList, setTrackOffset]);
return {
playTrackList,
trackOffset,
setNewPlaylist,
setNewPlaylistShuffle,
playInList,
play,
previous,
next,
};
};

View File

@ -0,0 +1,49 @@
import { useEffect, useState } from 'react';
import { Album, AlbumResource } from '@/back-api';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionServiceProps } from '@/service/session';
import { DataStoreType, useDataStore } from '@/utils/data-store';
export type AlbumServiceProps = {
store: DataStoreType<Album>;
};
export const useAlbumService = (): AlbumServiceProps => {
const { album } = useServiceContext();
return album;
};
export const useAlbumServiceWrapped = (
session: SessionServiceProps
): AlbumServiceProps => {
const store = useDataStore<Album>({
restApiName: 'ALBUM',
primaryKey: 'id',
getsCall: () => {
return AlbumResource.gets({
restConfig: session.getRestConfig(),
});
},
});
return { store };
};
export const useSpecificAlbum = (id: number | undefined) => {
const [dataAlbum, setDataAlbum] = useState<Album | undefined>(undefined);
const { store } = useAlbumService();
useEffect(() => {
setDataAlbum(store.get(id));
}, [store.data]);
return { dataAlbum };
};
export const useAlbumOfAnArtist = (idArtist: number | undefined) => {
const [dataAlbum, setDataAlbum] = useState<Album | undefined>(undefined);
const { store } = useAlbumService();
useEffect(() => {
setDataAlbum(store.get(idArtist));
}, [store.data]);
return { dataAlbum };
};

View File

@ -0,0 +1,40 @@
import { useEffect, useState } from 'react';
import { Artist, ArtistResource } from '@/back-api';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionServiceProps } from '@/service/session';
import { DataStoreType, useDataStore } from '@/utils/data-store';
export type ArtistServiceProps = {
store: DataStoreType<Artist>;
};
export const useArtistService = (): ArtistServiceProps => {
const { artist } = useServiceContext();
return artist;
};
export const useArtistServiceWrapped = (
session: SessionServiceProps
): ArtistServiceProps => {
const store = useDataStore<Artist>({
restApiName: 'ARTIST',
primaryKey: 'id',
getsCall: () => {
return ArtistResource.gets({
restConfig: session.getRestConfig(),
});
},
});
return { store };
};
export const useSpecificArtist = (id: number | undefined) => {
const [dataArtist, setDataArtist] = useState<Artist | undefined>(undefined);
const { store } = useArtistService();
useEffect(() => {
setDataArtist(store.get(id));
}, [store.data]);
return { dataArtist };
};

View File

@ -0,0 +1,101 @@
import { useDataStore } from '@/utils/data-store';
import { DataTools, TypeCheck } from '@/utils/data-tools';
import { isNullOrUndefined } from '@/utils/validator';
export class GenericDataService<TYPE> {
constructor(protected dataStore: DataStore<TYPE>) {}
gets(): Promise<TYPE[]> {
return this.dataStore.getData();
}
get(id: number): Promise<TYPE> {
let self = this;
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.get(response, id);
if (isNullOrUndefined(data)) {
reject('Data does not exist in the local BDD');
return;
}
resolve(data);
return;
})
.catch((response) => {
reject(response);
});
});
}
getAll(ids: number[]): Promise<TYPE[]> {
let self = this;
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.getsWhere(response, [
{
check: TypeCheck.EQUAL,
key: 'id',
value: ids,
},
]);
resolve(data);
return;
})
.catch((response) => {
reject(response);
});
});
}
getLike(value: string): Promise<TYPE[]> {
let self = this;
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.getNameLike(response, value);
if (isNullOrUndefined(data) || data.length === 0) {
reject('Data does not exist in the local BDD');
return;
}
resolve(data);
return;
})
.catch((response) => {
reject(response);
});
});
}
getOrder(): Promise<TYPE[]> {
let self = this;
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.getsWhere(
response,
[
{
check: TypeCheck.NOT_EQUAL,
key: 'id',
value: [undefined, null],
},
],
['name', 'id']
);
resolve(data);
})
.catch((response) => {
console.log(
`[E] ${self.constructor.name}: can not retrieve BDD values`
);
reject(response);
});
});
}
}

View File

@ -0,0 +1,96 @@
import { ReactNode, createContext, useContext, useMemo } from 'react';
import {
ActivePlaylistServiceProps,
useActivePlaylistServiceWrapped,
} from '@/service/ActivePlaylist';
import { AlbumServiceProps, useAlbumServiceWrapped } from '@/service/Album';
import { ArtistServiceProps, useArtistServiceWrapped } from '@/service/Artist';
import { SessionState } from '@/service/SessionState';
import { TrackServiceProps, useTrackServiceWrapped } from '@/service/Track';
import {
RightPart,
SessionServiceProps,
getRestConfig,
useSessionServiceWrapped,
} from '@/service/session';
export type ServiceContextType = {
session: SessionServiceProps;
track: TrackServiceProps;
artist: ArtistServiceProps;
album: AlbumServiceProps;
activePlaylist: ActivePlaylistServiceProps;
};
export const ServiceContext = createContext<ServiceContextType>({
session: {
setToken: (token: string) => {},
clearToken: () => {},
hasReadRight: (part: RightPart) => false,
hasWriteRight: (part: RightPart) => false,
state: SessionState.NO_USER,
getRestConfig: getRestConfig,
},
track: {
store: {
data: [],
isLoading: true,
get: (value, key) => undefined,
error: undefined,
},
},
artist: {
store: {
data: [],
isLoading: true,
get: (value, key) => undefined,
error: undefined,
},
},
album: {
store: {
data: [],
isLoading: true,
get: (value, key) => undefined,
error: undefined,
},
},
activePlaylist: {
playTrackList: [],
trackOffset: undefined,
setNewPlaylist: (listIds: number[]) => {},
setNewPlaylistShuffle: (listIds: number[]) => {},
playInList: (id: number, listIds: number[]) => {},
play: (id: number) => {},
},
});
export const useServiceContext = () => useContext(ServiceContext);
export const ServiceContextProvider = ({
children,
}: {
children: ReactNode;
}) => {
const session = useSessionServiceWrapped();
const track = useTrackServiceWrapped(session);
const artist = useArtistServiceWrapped(session);
const album = useAlbumServiceWrapped(session);
const activePlaylist = useActivePlaylistServiceWrapped();
const contextObjectData = useMemo(
() => ({
session,
track,
artist,
album,
activePlaylist,
}),
[session, track, artist, album]
);
return (
<ServiceContext.Provider value={contextObjectData}>
{children}
</ServiceContext.Provider>
);
};

View File

@ -0,0 +1,7 @@
export enum SessionState {
NO_USER,
CONNECTING,
CONNECTION_FAIL,
CONNECTED,
DISCONNECT,
}

180
front2/src/service/Track.ts Normal file
View File

@ -0,0 +1,180 @@
import { useCallback, useEffect, useState } from 'react';
import { Track, TrackResource } from '@/back-api';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionServiceProps } from '@/service/session';
import { DataStoreType, useDataStore } from '@/utils/data-store';
import { DataTools, TypeCheck } from '@/utils/data-tools';
import { isArrayOf, isNumber } from '@/utils/validator';
export type TrackServiceProps = {
store: DataStoreType<Track>;
};
export const useTrackService = (): TrackServiceProps => {
const { track } = useServiceContext();
return track;
};
export const useTrackServiceWrapped = (
session: SessionServiceProps
): TrackServiceProps => {
const store = useDataStore<Track>({
restApiName: 'TRACK',
primaryKey: 'id',
getsCall: () => {
return TrackResource.gets({
restConfig: session.getRestConfig(),
});
},
});
return { store };
};
export const useSpecificTrack = (id: number | undefined) => {
const [dataTrack, setDataTrack] = useState<Track | undefined>(undefined);
const { store } = useTrackService();
useEffect(() => {
console.log(`retrieve specific track: ${id}`);
setDataTrack(store.get(id));
}, [store.data]);
const updateTrackId = useCallback(
(id: number | undefined) => {
console.log(`retrieve specific track (update): ${id}`);
setDataTrack(store.get(id));
},
[setDataTrack, store]
);
return { dataTrack, updateTrackId };
};
/**
* Get all the track for a specific artist
* @param idArtist - Id of the artist.
* @returns a promise on the list of track elements
*/
export const useTracksOfAnArtist = (idArtist?: number) => {
const [tracksOnAnArtist, setTracksOnAnArtist] = useState<Track[]>([]);
const { store } = useTrackService();
useEffect(() => {
if (idArtist) {
setTracksOnAnArtist(
DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.CONTAINS,
key: 'artists',
value: idArtist,
},
],
['track', 'name']
)
);
}
}, [store.data]);
return { tracksOnAnArtist };
};
/**
* Get the number of track in this artist ID
* @param id - Id of the artist.
* @returns The number of track present in this artist
*/
export const useCountTracksOfAnArtist = (idArtist: number) => {
const { tracksOnAnArtist } = useTracksOfAnArtist(idArtist);
const countTracksOnAnArtist = tracksOnAnArtist.length;
return { countTracksOnAnArtist };
};
/**
* Get all the album of a specific artist
* @param artistId - ID of the artist
* @returns the required List.
*/
export const useAlbumIdsOfAnArtist = (idArtist?: number) => {
const [albumIdsOfAnArtist, setAlbumIdsOfAnArtist] = useState<number[]>([]);
const { tracksOnAnArtist } = useTracksOfAnArtist(idArtist);
useEffect(() => {
// extract a single time all value "id" in an array
const listAlbumId = DataTools.extractLimitOne(tracksOnAnArtist, 'albumId');
if (isArrayOf(listAlbumId, isNumber)) {
setAlbumIdsOfAnArtist(listAlbumId);
} else {
console.log(
'Fail to parse the result of the list value (impossible case)'
);
setAlbumIdsOfAnArtist([]);
}
}, [tracksOnAnArtist]);
return { albumIdsOfAnArtist };
};
export const useTracksOfAnAlbum = (idAlbum?: number) => {
const [tracksOnAnAlbum, setTracksOnAnAlbum] = useState<Track[]>([]);
const { store } = useTrackService();
useEffect(() => {
if (idAlbum) {
setTracksOnAnAlbum(
DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.EQUAL,
key: 'albumId',
value: idAlbum,
},
],
['track', 'name', 'id']
)
);
}
}, [store.data]);
return { tracksOnAnAlbum };
};
/**
* Get the number of track in this season ID
* @param AlbumId - Id of the album.
* @returns The number of element present in this season
*/
export const useCountTracksWithAlbumId = (albumId: number) => {
const { tracksOnAnAlbum } = useTracksOfAnAlbum(albumId);
const countTracksOnAnArtist = tracksOnAnAlbum.length;
return { countTracksOnAnArtist };
};
/**
* Get all the track for a specific artist
* @param idArtist - Id of the artist.
* @returns a promise on the list of track elements
*/
export const useTracksOfArtistWithNoAlbum = (idArtist: number) => {
const [tracksOnAnArtistWithNoAlbum, setTracksOnAnArtistWithNoAlbum] =
useState<Track[]>([]);
const { store } = useTrackService();
useEffect(() => {
if (idArtist) {
setTracksOnAnArtistWithNoAlbum(
DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.CONTAINS,
key: 'artists',
value: idArtist,
},
{
check: TypeCheck.EQUAL,
key: 'albumId',
value: undefined,
},
],
['track', 'name']
)
);
}
}, [store.data]);
return { tracksOnAnArtistWithNoAlbum };
};

View File

@ -0,0 +1,139 @@
import { useCallback, useEffect, useState } from 'react';
import { PartRight, RESTConfig, UserMe, UserResource } from '@/back-api';
import { environment, getApiUrl } from '@/environment';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionState } from '@/service/SessionState';
import { isBrowser } from '@/utils/layout';
import { parseToken } from '@/utils/sso';
const TOKEN_KEY = 'karusic-token-key-storage';
export const USERS = {
ADMIN:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIiwiYXBwbGljYXRpb24iOiJrYXJ1c2ljIiwiaXNzIjoiS2FyQXV0aCIsInJpZ2h0Ijp7ImthcnVzaWMiOnsiQURNSU4iOnRydWUsIlVTRVIiOnRydWV9fSwibG9naW4iOiJIZWVyb1l1aSIsImV4cCI6MTcyNDIwNjc5NCwiaWF0IjoxNzI0MTY2ODM0fQ.TEST_SIGNATURE_FOR_LOCAL_TEST_AND_TEST_E2E',
NO_USER: '',
} as const;
export const getUserToken = () => {
return localStorage.getItem(TOKEN_KEY);
};
export type RightPart = 'ADMIN' | 'USER';
export function getRestConfig(): RESTConfig {
return {
server: getApiUrl(),
token: getUserToken() ?? '',
};
}
export type SessionServiceProps = {
token?: string;
setToken: (token: string) => void;
clearToken: () => void;
login?: string;
hasReadRight: (part: RightPart) => boolean;
hasWriteRight: (part: RightPart) => boolean;
state: SessionState;
getRestConfig: () => RESTConfig;
};
export const useSessionService = (): SessionServiceProps => {
const { session } = useServiceContext();
return session;
};
export const useSessionServiceWrapped = (): SessionServiceProps => {
const [token, setToken] = useState<string | undefined>(
isBrowser ? (localStorage.getItem(TOKEN_KEY) ?? undefined) : undefined
);
const [state, setState] = useState<SessionState>(SessionState.NO_USER);
const [config, setConfig] = useState<UserMe | undefined>(undefined);
const updateRight = useCallback(() => {
if (isBrowser) {
console.log('Detect a new token...');
setState(SessionState.NO_USER);
setConfig(undefined);
if (token === undefined) {
console.log(` ==> No User`);
setState(SessionState.NO_USER);
localStorage.removeItem(TOKEN_KEY);
} else if (token === '__LOGOUT__') {
console.log(` ==> disconnection: ${token}`);
setState(SessionState.DISCONNECT);
localStorage.removeItem(TOKEN_KEY);
} else if (!['__LOGOUT__', '__FAIL__', '__CANCEL__'].includes(token)) {
console.log(' ==> Login ... (try to keep right)');
setState(SessionState.CONNECTING);
localStorage.setItem(TOKEN_KEY, token);
UserResource.getMe({
restConfig: getRestConfig(),
})
.then((response: UserMe) => {
console.log(` ==> New right arrived to '${response.login}'`);
setState(SessionState.CONNECTED);
setConfig(response);
})
.catch((error) => {
setState(SessionState.CONNECTION_FAIL);
console.log(` ==> Fail to get right: '${error}'`);
localStorage.removeItem(TOKEN_KEY);
});
}
}
}, [localStorage, parseToken, token]);
const setTokenLocal = useCallback(
(token: string) => {
setToken(token);
updateRight();
},
[updateRight, setToken]
);
const clearToken = useCallback(() => {
setToken(undefined);
updateRight();
}, [updateRight, setToken]);
const hasReadRight = useCallback(
(part: RightPart) => {
const right = config?.rights[environment.applName];
if (right === undefined) {
return false;
}
return [PartRight.READ, PartRight.READ_WRITE].includes(right[part]);
},
[config]
);
const hasWriteRight = useCallback(
(part: RightPart) => {
const right = config?.rights[environment.applName];
if (right === undefined) {
return false;
}
return [PartRight.READ, PartRight.READ_WRITE].includes(right[part]);
},
[config]
);
const getRestConfig = useCallback((): RESTConfig => {
return {
server: getApiUrl(),
token: token ?? '',
};
}, [token]);
useEffect(() => {
updateRight();
}, [updateRight]);
return {
token,
setToken: setTokenLocal,
clearToken,
login: config?.login,
hasReadRight,
hasWriteRight,
state,
getRestConfig,
};
};

65
front2/src/test/setup.ts Normal file
View File

@ -0,0 +1,65 @@
import '@testing-library/jest-dom';
import mediaQuery from 'css-mediaquery';
window.scrollTo = () => undefined;
global.matchMedia =
global.matchMedia ||
function (query) {
const instance = {
matches: mediaQuery.match(query, {
width: window.innerWidth,
height: window.innerHeight,
}),
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
};
// Listen to resize events from window.resizeTo and update the instance's match
window.addEventListener('resize', () => {
const change = mediaQuery.match(query, {
width: window.innerWidth,
height: window.innerHeight,
});
// eslint-disable-next-line
if (change != instance.matches) {
instance.matches = change;
instance.dispatchEvent('change');
}
});
return instance;
};
// Mock window.resizeTo's impl.
Object.defineProperty(window, 'resizeTo', {
value: (width, height) => {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: width,
});
Object.defineProperty(window, 'outerWidth', {
configurable: true,
writable: true,
value: width,
});
Object.defineProperty(window, 'innerHeight', {
configurable: true,
writable: true,
value: height,
});
Object.defineProperty(window, 'outerHeight', {
configurable: true,
writable: true,
value: height,
});
window.dispatchEvent(new Event('resize'));
},
});

27
front2/src/test/utils.tsx Normal file
View File

@ -0,0 +1,27 @@
import { ReactElement, ReactNode } from 'react';
import { ChakraProvider } from '@chakra-ui/react';
import { RenderOptions, render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import theme from '@/theme';
const CustomWrapper = ({ children }: { children: ReactNode }) => {
return (
<ChakraProvider theme={theme}>
<BrowserRouter>{children}</BrowserRouter>
</ChakraProvider>
);
};
const customRender = (ui: ReactElement, options: RenderOptions = {}) =>
render(ui, {
wrapper: CustomWrapper,
...options,
});
// re-export everything
export * from '@testing-library/react';
// override render method
export { customRender as render };

View File

@ -0,0 +1,24 @@
export default {
sizes: {
'2xs': {
fontSize: '0.5em',
},
xs: {
fontSize: '0.6em',
},
sm: {
fontSize: '0.7em',
},
md: {
fontSize: '0.8em',
textTransform: 'none',
},
lg: {
fontSize: '0.9em',
textTransform: 'none',
},
},
defaultProps: {
size: 'md',
},
};

View File

@ -0,0 +1,187 @@
import { border, defineStyleConfig, keyframes } from '@chakra-ui/react';
import { isAccessible, mode, transparentize } from '@chakra-ui/theme-tools';
import { shadows } from '@/theme/foundations/shadows';
const shimmer = keyframes`
100% {
transform: translateX(100%);
}
`;
const customVariant = ({
theme,
bg,
bgHover = bg,
bgActive = bgHover,
color,
colorHover = color,
boxColorFocus = '#0x000000',
boxShadowHover = 'outline-over',
}) => {
const isColorAccessible = isAccessible(color, bg, {
size: 'large',
level: 'AA',
})(theme);
return {
bg,
color: isColorAccessible ? color : 'black',
border: '1px solid #00000000',
_focus: {
//border: `1px solid ${boxColorFocus}`,
border: `1px solid #000000`,
},
_hover: {
bg: bgHover,
color: isColorAccessible ? colorHover : 'black',
boxShadow: boxShadowHover,
_disabled: {
bg,
boxShadow: 'none',
},
},
_active: { bg: bgActive },
};
};
export default defineStyleConfig({
variants: {
// Custom variants
'@primary': (props) =>
customVariant({
theme: props.theme,
bg: mode('brand.600', 'brand.300')(props),
bgHover: mode('brand.700', 'brand.400')(props),
bgActive: mode('brand.600', 'brand.300')(props),
color: mode('white', 'brand.900')(props),
boxColorFocus: mode('brand.100', 'brand.600')(props),
}),
'@secondary': (props) =>
customVariant({
theme: props.theme,
bg: mode('brand.100', 'brand.900')(props),
bgHover: mode('brand.200', 'brand.800')(props),
bgActive: mode('brand.300', 'brand.700')(props),
color: mode('brand.700', 'brand.50')(props),
colorHover: mode('brand.800', 'brand.100')(props),
boxColorFocus: mode('brand.900', 'brand.300')(props),
}),
'@danger': (props) =>
customVariant({
theme: props.theme,
bg: mode('error.600', 'error.300')(props),
bgHover: mode('error.700', 'error.400')(props),
bgActive: mode('error.600', 'error.300')(props),
color: mode('white', 'error.900')(props),
boxColorFocus: mode('error.900', 'error.500')(props),
}),
'@progress': (props) => ({
...customVariant({
theme: props.theme,
bg: mode(`${props.colorScheme}.500`, `${props.colorScheme}.300`)(props),
bgHover: mode(
`${props.colorScheme}.600`,
`${props.colorScheme}.400`
)(props),
bgActive: mode(
`${props.colorScheme}.700`,
`${props.colorScheme}.500`
)(props),
color: mode('white', `${props.colorScheme}.900`)(props),
boxColorFocus: mode(
`${props.colorScheme}.900`,
`${props.colorScheme}.600`
)(props),
}),
overflow: 'hidden',
_after: !props.isLoading
? {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
transform: 'translateX(-100%)',
bgGradient: `linear(90deg, ${transparentize(
`${props.colorScheme}.100`,
0
)(props.theme)} 0, ${transparentize(
`${props.colorScheme}.100`,
0.2
)(props.theme)} 20%, ${transparentize(
`${props.colorScheme}.100`,
0.5
)(props.theme)} 60%, ${transparentize(
`${props.colorScheme}.100`,
0
)(props.theme)})`,
animation: `${shimmer} 3s infinite`,
content: '""',
}
: undefined,
}),
'@menu': (props) => ({
bg: mode('back.100', 'back.800')(props),
color: mode('brand.900', 'brand.100')(props),
borderRadius: 0,
border: 0,
_hover: { background: mode('back.300', 'back.600')(props) },
_focus: { border: 'none' },
fontSize: '20px',
textTransform: 'uppercase',
}),
// Default variants
solid: (props) => ({
bg:
props.colorScheme === 'gray'
? mode('gray.100', 'whiteAlpha.100')(props)
: `${props.colorScheme}.600`,
_hover: {
bg:
props.colorScheme === 'gray'
? mode('gray.200', 'whiteAlpha.200')(props)
: `${props.colorScheme}.700`,
},
_focus: {
boxShadow: `outline-${props.colorScheme}`,
},
}),
light: (props) => ({
bg:
props.colorScheme === 'gray'
? mode('gray.100', 'whiteAlpha.100')(props)
: `${props.colorScheme}.100`,
color:
props.colorScheme === 'gray'
? mode('gray.700', 'whiteAlpha.700')(props)
: `${props.colorScheme}.700`,
_hover: {
bg:
props.colorScheme === 'gray'
? mode('gray.200', 'whiteAlpha.200')(props)
: `${props.colorScheme}.200`,
color:
props.colorScheme === 'gray'
? mode('gray.800', 'whiteAlpha.800')(props)
: `${props.colorScheme}.800`,
},
_focus: {
boxShadow: `outline-${props.colorScheme}`,
},
}),
ghost: (props) => ({
bg: transparentize(`${props.colorScheme}.50`, 0.05)(props.theme),
borderRadius: 'full',
_hover: {
bg: transparentize(`${props.colorScheme}.50`, 0.15)(props.theme),
},
_focus: {
boxShadow: 'none',
},
}),
},
});

View File

@ -0,0 +1,5 @@
export default {
defaultProps: {
colorScheme: 'brand',
},
};

View File

@ -0,0 +1,19 @@
import { defineStyleConfig } from '@chakra-ui/react';
import { mode } from '@chakra-ui/theme-tools';
const flexTheme = defineStyleConfig({
variants: {
'@menu': (props) => ({
bg: mode('back.100', 'back.800')(props),
color: mode('brand.900', 'brand.100')(props),
borderRadius: 0,
border: 0,
_hover: { background: mode('back.300', 'back.600')(props) },
_focus: { border: 'none' },
fontSize: '20px',
}),
},
});
export default flexTheme;

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