mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 06:55:51 +01:00
🔀 Merge branch 'dev' into feature/add-basic-authentication
This commit is contained in:
42
.github/workflows/docker_dev.yml
vendored
42
.github/workflows/docker_dev.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=raw,value=${{ github.event.inputs.tag }}, prefix=test-,enable=${{ github.event.inputs.tag != '' }}
|
||||
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event.inputs.tag != '' }}
|
||||
tpye=raw,value=dev,priority=1,enable=${{ github.event.inputs.tag == '' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
@@ -114,43 +114,3 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Restore next build
|
||||
uses: actions/cache@v3
|
||||
id: restore-build-cache
|
||||
env:
|
||||
cache-name: cache-next-build
|
||||
with:
|
||||
# if you use a custom build directory, replace all instances of `.next` in this file with your build directory
|
||||
# ex: if your app builds to `dist`, replace `.next` with `dist`
|
||||
path: .next/cache
|
||||
# change this if you prefer a more strict cache
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}
|
||||
|
||||
- run: yarn install
|
||||
|
||||
- name: Build next.js app
|
||||
# change this if your site requires a custom build command
|
||||
run: yarn turbo build
|
||||
@@ -30,6 +30,7 @@ module.exports = {
|
||||
'tr',
|
||||
'lv',
|
||||
'hr',
|
||||
'hu'
|
||||
],
|
||||
|
||||
localeDetection: true,
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"js-file-download": "^0.4.12",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"next": "13.4.10",
|
||||
"next": "13.4.19",
|
||||
"next-auth": "^4.22.3",
|
||||
"next-i18next": "^14.0.0",
|
||||
"nzbget-api": "^0.0.3",
|
||||
@@ -111,7 +111,7 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||
"@types/cookies": "^0.7.7",
|
||||
"@types/dockerode": "^3.3.9",
|
||||
"@types/node": "18.16.19",
|
||||
"@types/node": "18.17.8",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react": "^18.2.11",
|
||||
"@types/uuid": "^9.0.0",
|
||||
|
||||
@@ -14,5 +14,12 @@
|
||||
"category": {
|
||||
"openAllInNewTab": "Open all in new tab"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"moveUp": "Move up",
|
||||
"moveDown": "Move down",
|
||||
"addCategory": "Add category",
|
||||
"addAbove": "above",
|
||||
"addBelow": "below"
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,19 @@
|
||||
"goBack": "Go back to the previous step",
|
||||
"actionIcon": {
|
||||
"tooltip": "Add a tile"
|
||||
},
|
||||
"apps": "Apps",
|
||||
"app": {
|
||||
"defaultName": "Your App"
|
||||
},
|
||||
"widgets": "Widgets",
|
||||
"categories": "Categories",
|
||||
"category": {
|
||||
"newName": "Name of new category",
|
||||
"defaultName": "New Category",
|
||||
"created": {
|
||||
"title": "Category created",
|
||||
"message": "The category \"{{name}}\" has been created"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
"key": "Shortcut key",
|
||||
"action": "Action",
|
||||
"keybinds": "Keybinds",
|
||||
"actions": {
|
||||
"toggleTheme": "Toggle light/dark mode",
|
||||
"focusSearchBar": "Focus on search bar",
|
||||
"openDocker": "Open docker Widget",
|
||||
"toggleEdit": "Toggle Edit Mode"
|
||||
},
|
||||
"metrics": {
|
||||
"configurationSchemaVersion": "Configuration schema version",
|
||||
"configurationsCount": "Available configurations",
|
||||
@@ -15,4 +21,5 @@
|
||||
"locales": "Configured I18n locales",
|
||||
"experimental_disableEditMode": "<b>EXPERIMENTAL</b>: Disable edit mode"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -99,6 +99,12 @@
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"popover": "Your form contains invalid data. Hence, it can't be saved. Please resolve all issues and click this button again to save your changes"
|
||||
"popover": "Your form contains invalid data. Hence, it can't be saved. Please resolve all issues and click this button again to save your changes",
|
||||
"name": "Name is required",
|
||||
"noUrl": "Url is required",
|
||||
"invalidUrl": "Value is not a valid url",
|
||||
"noIconUrl": "This field is required",
|
||||
"noExternalUri": "External URI is required",
|
||||
"invalidExternalUri": "External URI is not a valid uri"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
"label": "Items"
|
||||
},
|
||||
"layout": {
|
||||
"label": "Layout"
|
||||
"label": "Layout",
|
||||
"data":{
|
||||
"autoGrid": "Auto Grid",
|
||||
"horizontal": "Horizontal",
|
||||
"vertical": "Vertical"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -21,5 +26,21 @@
|
||||
"title": "Bookmark list empty",
|
||||
"text": "Add new items to this list in the edit mode"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"validation": {
|
||||
"length100": "Length must be between 1 and 100",
|
||||
"length200": "Length must be between 1 and 200",
|
||||
"length400": "Length must be between 1 and 400",
|
||||
"invalidLink": "Not a valid link",
|
||||
"errorMsg": "Did not save, because there were validation errors. Please adust your inputs"
|
||||
},
|
||||
"name": "Name",
|
||||
"url": "URL",
|
||||
"newTab": "Open in new tab",
|
||||
"hideHostname": "Hide Hostname",
|
||||
"hideIcon": "Hide Icon",
|
||||
"delete": "Delete"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,25 @@
|
||||
"label": "Use Sonarr v4 API"
|
||||
},
|
||||
"radarrReleaseType": {
|
||||
"label": "Radarr release type"
|
||||
"label": "Radarr release type",
|
||||
"data":{
|
||||
"inCinemas": "In Cinemas",
|
||||
"physicalRelease": "Physical",
|
||||
"digitalRelease": "Digital"
|
||||
}
|
||||
},
|
||||
"hideWeekDays": {
|
||||
"label": "Hide week days"
|
||||
},
|
||||
"fontSize": {
|
||||
"label": "Font Size"
|
||||
"label": "Font Size",
|
||||
"data":{
|
||||
"xs": "Extra Small",
|
||||
"sm": "Small",
|
||||
"md": "Medium",
|
||||
"lg": "Large",
|
||||
"xl": "Extra Large"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
"noUrl": {
|
||||
"title": "Invalid URL",
|
||||
"text": "Ensure that you've entered a valid address in the configuration of your widget"
|
||||
}
|
||||
},
|
||||
"browserSupport": "Your Browser does not support iframes. Please update your browser."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,37 @@
|
||||
},
|
||||
"tooltips": {
|
||||
"approve": "Approve requests",
|
||||
"decline": "Decline requests"
|
||||
"decline": "Decline requests",
|
||||
"approving": "Approving Request..."
|
||||
},
|
||||
"mutation": {
|
||||
"approving": "Approving",
|
||||
"declining": "Declining",
|
||||
"request": "request...",
|
||||
"approved": "Request was approved!",
|
||||
"declined": "Request was declined!"
|
||||
},
|
||||
"detail": {
|
||||
"label": "Stats for nerds",
|
||||
"id": "ID",
|
||||
"device": "Device",
|
||||
"video": {
|
||||
"video":"Video",
|
||||
"resolution": "Resolution",
|
||||
"framerate": "Framerate",
|
||||
"codec": "Video Codec"
|
||||
},
|
||||
"audio": {
|
||||
"audio": "Audio",
|
||||
"channels": "Audio Channels",
|
||||
"codec": "Audio Codec"
|
||||
},
|
||||
"transcoding": {
|
||||
"transcoding": "Transcoding",
|
||||
"context": "Context",
|
||||
"requested": "Hardware Encoding Requested",
|
||||
"source": "Source Codec",
|
||||
"target": "Target Codec"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
"settings": {
|
||||
"title": "Media requests stats",
|
||||
"direction": {
|
||||
"label": "Direction of the layout."
|
||||
"label": "Direction of the layout.",
|
||||
"data":{
|
||||
"row": "Horizontal",
|
||||
"column": "Vertical"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"title": "Settings for media server widget"
|
||||
}
|
||||
},
|
||||
"loading": "Loading streams",
|
||||
"card": {
|
||||
"table": {
|
||||
"header": {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"label": "Refresh interval (in minutes)"
|
||||
},
|
||||
"dangerousAllowSanitizedItemContent": {
|
||||
"label": ""
|
||||
"label": "Allow HTML formatting (Dangerous)",
|
||||
"info": "Allowing HTML formatting from outside could be dangerous.<br/>Please make sure that the feed is from a trusted source."
|
||||
},
|
||||
"textLinesClamp": {
|
||||
"label": "Text lines clamp"
|
||||
|
||||
@@ -59,11 +59,12 @@
|
||||
},
|
||||
"generic": {
|
||||
"title": "An unexpected error occurred",
|
||||
"text": "Homarr was unable to communicate with your Torrent clients. Please check your configuration"
|
||||
"text": "Unable to communicate with your Torrent clients. Please check your configuration"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"title": "Loading..."
|
||||
"title": "Loading",
|
||||
"description": "Establishing a connection"
|
||||
},
|
||||
"popover": {
|
||||
"introductionPrefix": "Managed by",
|
||||
|
||||
@@ -46,4 +46,4 @@
|
||||
},
|
||||
"paused": "Paused"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,5 +32,6 @@
|
||||
"thunderstormWithHail": "Thunderstorm with hail",
|
||||
"unknown": "Unknown"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": "An error occured"
|
||||
}
|
||||
|
||||
@@ -47,14 +47,14 @@ export const EditAppModal = ({
|
||||
const form = useForm<AppType>({
|
||||
initialValues: innerProps.app,
|
||||
validate: {
|
||||
name: (name) => (!name ? 'Name is required' : null),
|
||||
name: (name) => (!name ? t('validation.name') : null),
|
||||
url: (url) => {
|
||||
if (!url) {
|
||||
return 'Url is required';
|
||||
return t('validation.noUrl');
|
||||
}
|
||||
|
||||
if (!url.match(appUrlRegex)) {
|
||||
return 'Value is not a valid url';
|
||||
return t('validation.invalidUrl');
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -62,7 +62,7 @@ export const EditAppModal = ({
|
||||
appearance: {
|
||||
iconUrl: (url: string) => {
|
||||
if (url.length < 1) {
|
||||
return 'This field is required';
|
||||
return t('validation.noIconUrl');
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -71,11 +71,11 @@ export const EditAppModal = ({
|
||||
behaviour: {
|
||||
externalUrl: (url: string) => {
|
||||
if (url === undefined || url.length < 1) {
|
||||
return 'External URI is required';
|
||||
return t('validation.noExternalUri');
|
||||
}
|
||||
|
||||
if (!url.match(appUrlWithAnyProtocolRegex)) {
|
||||
return 'External URI is not a valid uri';
|
||||
return t('validation.invalidExternalUri');
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -33,12 +33,12 @@ export const AvailableElementTypes = ({
|
||||
const onClickCreateCategory = async () => {
|
||||
openContextModalGeneric<CategoryEditModalInnerProps>({
|
||||
modal: 'categoryEditModal',
|
||||
title: 'Name of new category',
|
||||
title: t('category.newName'),
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
category: {
|
||||
id: uuidv4(),
|
||||
name: 'New category',
|
||||
name: t('category.defaultName'),
|
||||
position: 0, // doesn't matter, is being overwritten
|
||||
},
|
||||
onSuccess: async (category) => {
|
||||
@@ -65,8 +65,8 @@ export const AvailableElementTypes = ({
|
||||
})).then(() => {
|
||||
closeModal(modalId);
|
||||
showNotification({
|
||||
title: 'Category created',
|
||||
message: `The category ${category.name} has been created`,
|
||||
title: t('category.created.title'),
|
||||
message: t('category.created.message', { name: category.name}),
|
||||
color: 'teal',
|
||||
});
|
||||
});
|
||||
@@ -81,7 +81,7 @@ export const AvailableElementTypes = ({
|
||||
<Space h="lg" />
|
||||
<Group spacing="md" grow>
|
||||
<ElementItem
|
||||
name="Apps"
|
||||
name={t('apps')}
|
||||
icon={<IconBox size={40} strokeWidth={1.3} />}
|
||||
onClick={() => {
|
||||
openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({
|
||||
@@ -89,7 +89,7 @@ export const AvailableElementTypes = ({
|
||||
innerProps: {
|
||||
app: {
|
||||
id: uuidv4(),
|
||||
name: 'Your app',
|
||||
name: t('app.defaultName'),
|
||||
url: 'https://homarr.dev',
|
||||
appearance: {
|
||||
iconUrl: '/imgs/logo/logo.png',
|
||||
@@ -126,12 +126,12 @@ export const AvailableElementTypes = ({
|
||||
}}
|
||||
/>
|
||||
<ElementItem
|
||||
name="Widgets"
|
||||
name={t('widgets')}
|
||||
icon={<IconStack size={40} strokeWidth={1.3} />}
|
||||
onClick={onOpenWidgets}
|
||||
/>
|
||||
<ElementItem
|
||||
name="Category"
|
||||
name={t('categories')}
|
||||
icon={<IconBoxAlignTop size={40} strokeWidth={1.3} />}
|
||||
onClick={onClickCreateCategory}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { CategoryType } from '../../../../types/category';
|
||||
import { useCategoryActions } from './useCategoryActions';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
interface CategoryEditMenuProps {
|
||||
category: CategoryType;
|
||||
@@ -21,6 +22,7 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
|
||||
const { name: configName } = useConfigContext();
|
||||
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit, remove } =
|
||||
useCategoryActions(configName, category);
|
||||
const { t } = useTranslation(['layout/common','common']);
|
||||
|
||||
return (
|
||||
<Menu withinPortal withArrow>
|
||||
@@ -31,24 +33,28 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item icon={<IconEdit size={20} />} onClick={edit}>
|
||||
Edit
|
||||
{t('common:edit')}
|
||||
</Menu.Item>
|
||||
<Menu.Item icon={<IconTrash size={20} />} onClick={remove}>
|
||||
Remove
|
||||
{t('common:remove')}
|
||||
</Menu.Item>
|
||||
<Menu.Label>Change positon</Menu.Label>
|
||||
<Menu.Label>
|
||||
{t('common:changePosition')}
|
||||
</Menu.Label>
|
||||
<Menu.Item icon={<IconTransitionTop size={20} />} onClick={moveCategoryUp}>
|
||||
Move up
|
||||
{t('menu.moveUp')}
|
||||
</Menu.Item>
|
||||
<Menu.Item icon={<IconTransitionBottom size={20} />} onClick={moveCategoryDown}>
|
||||
Move down
|
||||
{t('menu.moveDown')}
|
||||
</Menu.Item>
|
||||
<Menu.Label>Add category</Menu.Label>
|
||||
<Menu.Label>
|
||||
{t('menu.addCategory')}
|
||||
</Menu.Label>
|
||||
<Menu.Item icon={<IconRowInsertTop size={20} />} onClick={addCategoryAbove}>
|
||||
Add category above
|
||||
{t('menu.addCategory') + ' ' + t('menu.addAbove')}
|
||||
</Menu.Item>
|
||||
<Menu.Item icon={<IconRowInsertBottom size={20} />} onClick={addCategoryBelow}>
|
||||
Add category below
|
||||
{t('menu.addCategory') + ' ' + t('menu.addBelow')}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@@ -53,10 +53,10 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
|
||||
const { t } = useTranslation(['common', 'layout/modals/about']);
|
||||
|
||||
const keybinds = [
|
||||
{ key: 'Mod + J', shortcut: 'Toggle light/dark mode' },
|
||||
{ key: 'Mod + K', shortcut: 'Focus on search bar' },
|
||||
{ key: 'Mod + B', shortcut: 'Open docker widget' },
|
||||
{ key: 'Mod + E', shortcut: 'Toggle Edit mode' },
|
||||
{ key: 'Mod + J', shortcut: t('layout/modals/about:actions.toggleTheme') },
|
||||
{ key: 'Mod + K', shortcut: t('layout/modals/about:actions.focusSearchBar') },
|
||||
{ key: 'Mod + B', shortcut: t('layout/modals/about:actions.openDocker') },
|
||||
{ key: 'Mod + E', shortcut: t('layout/modals/about:actions.toggleEdit') },
|
||||
];
|
||||
const rows = keybinds.map((element) => (
|
||||
<tr key={element.key}>
|
||||
|
||||
@@ -49,6 +49,6 @@ export async function getStaticProps({ req, res, locale }: GetServerSidePropsCon
|
||||
const useStyles = createStyles(() => ({
|
||||
image: {
|
||||
margin: '0 auto',
|
||||
display: 'blcok',
|
||||
display: 'block',
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -4,8 +4,10 @@ import { Notifications } from '@mantine/notifications';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import Consola from 'consola';
|
||||
import { getCookie, setCookie } from 'cookies-next';
|
||||
import dayjs from 'dayjs';
|
||||
import locale from 'dayjs/plugin/localeData';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import 'flag-icons/css/flag-icons.min.css';
|
||||
import moment from 'moment-timezone';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import { Session } from 'next-auth';
|
||||
import { SessionProvider, getSession } from 'next-auth/react';
|
||||
@@ -34,6 +36,9 @@ import {
|
||||
} from '../tools/server/getPackageVersion';
|
||||
import { theme } from '../tools/server/theme/theme';
|
||||
|
||||
dayjs.extend(locale);
|
||||
dayjs.extend(utc);
|
||||
|
||||
function App(
|
||||
this: any,
|
||||
props: AppProps<{
|
||||
@@ -53,8 +58,8 @@ function App(
|
||||
const { Component, pageProps } = props;
|
||||
// TODO: make mapping from our locales to moment locales
|
||||
const language = getLanguageByCode(pageProps.locale);
|
||||
require('moment/locale/' + language.momentLocale);
|
||||
moment.locale(language.momentLocale);
|
||||
require(`dayjs/locale/${language.locale}.js`);
|
||||
dayjs.locale(language.locale);
|
||||
|
||||
const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>(
|
||||
props.pageProps.primaryColor ?? 'red'
|
||||
|
||||
@@ -12,13 +12,21 @@ export const notebookRouter = createTRPCRouter({
|
||||
update: publicProcedure
|
||||
.input(z.object({ widgetId: z.string(), content: z.string(), configName: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
//TODO: #1305 Remove use of DISABLE_EDIT_MODE for auth update
|
||||
if (!process.env.DISABLE_EDIT_MODE) {
|
||||
throw new TRPCError({
|
||||
code: 'METHOD_NOT_SUPPORTED',
|
||||
message: 'Edit is not allowed, because edit mode is disabled'
|
||||
});
|
||||
}
|
||||
|
||||
const config = getConfig(input.configName);
|
||||
const widget = config.widgets.find((widget) => widget.id === input.widgetId) as
|
||||
| INotebookWidget
|
||||
| undefined;
|
||||
|
||||
if (!widget) {
|
||||
return new TRPCError({
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Specified widget was not found',
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ export type Language = {
|
||||
*/
|
||||
country?: string;
|
||||
|
||||
momentLocale: string;
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export const languages: Language[] = [
|
||||
@@ -20,7 +20,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'German',
|
||||
emoji: '🇩🇪',
|
||||
country: 'DE',
|
||||
momentLocale: 'de',
|
||||
locale: 'de',
|
||||
},
|
||||
{
|
||||
shortName: 'en',
|
||||
@@ -28,7 +28,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'English',
|
||||
emoji: '🇬🇧',
|
||||
country: 'GB',
|
||||
momentLocale: 'en-gb',
|
||||
locale: 'en-gb',
|
||||
},
|
||||
// Danish
|
||||
{
|
||||
@@ -37,7 +37,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Danish',
|
||||
emoji: '🇩🇰',
|
||||
country: 'DK',
|
||||
momentLocale: 'da',
|
||||
locale: 'da',
|
||||
},
|
||||
// Hebrew
|
||||
{
|
||||
@@ -46,7 +46,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Hebrew',
|
||||
emoji: '🇮🇱',
|
||||
country: 'IL',
|
||||
momentLocale: 'he',
|
||||
locale: 'he',
|
||||
},
|
||||
{
|
||||
shortName: 'es',
|
||||
@@ -54,7 +54,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Spanish',
|
||||
emoji: '🇪🇸',
|
||||
country: 'ES',
|
||||
momentLocale: 'es',
|
||||
locale: 'es',
|
||||
},
|
||||
{
|
||||
shortName: 'fr',
|
||||
@@ -62,7 +62,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'French',
|
||||
emoji: '🇫🇷',
|
||||
country: 'FR',
|
||||
momentLocale: 'fr',
|
||||
locale: 'fr',
|
||||
},
|
||||
{
|
||||
shortName: 'it',
|
||||
@@ -70,7 +70,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Italian',
|
||||
emoji: '🇮🇹',
|
||||
country: 'IT',
|
||||
momentLocale: 'it',
|
||||
locale: 'it',
|
||||
},
|
||||
{
|
||||
shortName: 'ja',
|
||||
@@ -78,7 +78,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Japanese',
|
||||
emoji: '🇯🇵',
|
||||
country: 'JP',
|
||||
momentLocale: 'jp'
|
||||
locale: 'ja',
|
||||
},
|
||||
{
|
||||
shortName: 'ko',
|
||||
@@ -86,14 +86,14 @@ export const languages: Language[] = [
|
||||
translatedName: 'Korean',
|
||||
emoji: '🇰🇷',
|
||||
country: 'KR',
|
||||
momentLocale: 'ko'
|
||||
locale: 'ko',
|
||||
},
|
||||
{
|
||||
shortName: 'lol',
|
||||
originalName: 'LOLCAT',
|
||||
translatedName: 'LOLCAT',
|
||||
emoji: '🐱',
|
||||
momentLocale: 'en-gb',
|
||||
locale: 'en-gb',
|
||||
},
|
||||
// Norwegian
|
||||
{
|
||||
@@ -102,7 +102,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Norwegian',
|
||||
emoji: '🇳🇴',
|
||||
country: 'NO',
|
||||
momentLocale: 'nb',
|
||||
locale: 'nb',
|
||||
},
|
||||
// Slovak
|
||||
{
|
||||
@@ -111,7 +111,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Slovak',
|
||||
emoji: '🇸🇰',
|
||||
country: 'SK',
|
||||
momentLocale: 'sk',
|
||||
locale: 'sk',
|
||||
},
|
||||
{
|
||||
shortName: 'nl',
|
||||
@@ -119,7 +119,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Dutch',
|
||||
emoji: '🇳🇱',
|
||||
country: 'NL',
|
||||
momentLocale: 'nl',
|
||||
locale: 'nl',
|
||||
},
|
||||
{
|
||||
shortName: 'pl',
|
||||
@@ -127,7 +127,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Polish',
|
||||
emoji: '🇵🇱',
|
||||
country: 'PL',
|
||||
momentLocale: 'pl',
|
||||
locale: 'pl',
|
||||
},
|
||||
{
|
||||
shortName: 'pt',
|
||||
@@ -135,7 +135,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Portuguese',
|
||||
emoji: '🇵🇹',
|
||||
country: 'PT',
|
||||
momentLocale: 'pt',
|
||||
locale: 'pt',
|
||||
},
|
||||
{
|
||||
shortName: 'ru',
|
||||
@@ -143,15 +143,15 @@ export const languages: Language[] = [
|
||||
translatedName: 'Russian',
|
||||
emoji: '🇷🇺',
|
||||
country: 'RU',
|
||||
momentLocale: 'ru',
|
||||
locale: 'ru',
|
||||
},
|
||||
{
|
||||
momentLocale: 'si',
|
||||
shortName: 'sl',
|
||||
originalName: 'Slovenščina',
|
||||
translatedName: 'Slovenian',
|
||||
emoji: '🇸🇮',
|
||||
country: 'SI'
|
||||
country: 'SI',
|
||||
locale: 'sl',
|
||||
},
|
||||
{
|
||||
shortName: 'sv',
|
||||
@@ -159,7 +159,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Swedish',
|
||||
emoji: '🇸🇪',
|
||||
country: 'SE',
|
||||
momentLocale: 'sv',
|
||||
locale: 'sv',
|
||||
},
|
||||
{
|
||||
shortName: 'uk',
|
||||
@@ -167,7 +167,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Ukrainian',
|
||||
emoji: '🇺🇦',
|
||||
country: 'UA',
|
||||
momentLocale: 'uk',
|
||||
locale: 'uk',
|
||||
},
|
||||
// Vietnamese
|
||||
{
|
||||
@@ -176,7 +176,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Vietnamese',
|
||||
emoji: '🇻🇳',
|
||||
country: 'VN',
|
||||
momentLocale: 'vi',
|
||||
locale: 'vi',
|
||||
},
|
||||
{
|
||||
shortName: 'zh',
|
||||
@@ -184,15 +184,15 @@ export const languages: Language[] = [
|
||||
translatedName: 'Chinese',
|
||||
emoji: '🇨🇳',
|
||||
country: 'CN',
|
||||
momentLocale: 'cn'
|
||||
locale: 'zh-cn',
|
||||
},
|
||||
{
|
||||
originalName: 'Ελληνικά',
|
||||
translatedName: 'Greek',
|
||||
emoji: '🇬🇷',
|
||||
country: 'GR',
|
||||
momentLocale: 'el',
|
||||
shortName: 'gr'
|
||||
shortName: 'gr',
|
||||
locale: 'el',
|
||||
},
|
||||
{
|
||||
shortName: 'tr',
|
||||
@@ -200,7 +200,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Turkish',
|
||||
emoji: '🇹🇷',
|
||||
country: 'TR',
|
||||
momentLocale: 'tr',
|
||||
locale: 'tr',
|
||||
},
|
||||
{
|
||||
shortName: 'lv',
|
||||
@@ -208,7 +208,7 @@ export const languages: Language[] = [
|
||||
translatedName: 'Latvian',
|
||||
emoji: '🇱🇻',
|
||||
country: 'LV',
|
||||
momentLocale: 'lv',
|
||||
locale: 'lv',
|
||||
},
|
||||
{
|
||||
shortName: 'hr',
|
||||
@@ -216,7 +216,15 @@ export const languages: Language[] = [
|
||||
translatedName: 'Croatian',
|
||||
emoji: '🇭🇷',
|
||||
country: 'HR',
|
||||
momentLocale: 'hr',
|
||||
locale: 'hr',
|
||||
},
|
||||
// Hungarian
|
||||
{
|
||||
shortName: 'hu',
|
||||
originalName: 'Magyar',
|
||||
translatedName: 'Hungarian',
|
||||
emoji: '🇭🇺',
|
||||
locale: 'hu',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export class PlexClient {
|
||||
const playerElement = this.findElement('Player', videoElement.elements);
|
||||
const mediaElement = this.findElement('Media', videoElement.elements);
|
||||
const sessionElement = this.findElement('Session', videoElement.elements);
|
||||
const transcodingElement = this.findElement('TranscodeSession', videoElement.elements);
|
||||
|
||||
if (!playerElement || !mediaElement) {
|
||||
return undefined;
|
||||
@@ -43,7 +44,6 @@ export class PlexClient {
|
||||
const { videoCodec, videoFrameRate, audioCodec, audioChannels, height, width, bitrate } =
|
||||
mediaElement;
|
||||
|
||||
const transcodingElement = this.findElement('TranscodeSession', videoElement.elements);
|
||||
|
||||
return {
|
||||
id: sessionElement?.id as string | undefined,
|
||||
@@ -51,7 +51,10 @@ export class PlexClient {
|
||||
userProfilePicture: userElement?.thumb as string | undefined,
|
||||
sessionName: `${playerElement.product} (${playerElement.title})`,
|
||||
currentlyPlaying: {
|
||||
name: videoElement.attributes?.title as string,
|
||||
name: `${videoElement.attributes?.grandparentTitle ?? videoElement.attributes?.title}`,
|
||||
seasonName: videoElement.attributes?.parentTitle,
|
||||
episodeName: videoElement.attributes?.title,
|
||||
episodeCount: videoElement.attributes?.index ?? undefined,
|
||||
type: this.getCurrentlyPlayingType(videoElement.attributes?.type as string),
|
||||
metadata: {
|
||||
video: {
|
||||
|
||||
@@ -74,6 +74,7 @@ const definition = defineWidget({
|
||||
};
|
||||
},
|
||||
itemComponent({ data, onChange, delete: deleteData }) {
|
||||
const { t } = useTranslation('modules/bookmark');
|
||||
const form = useForm({
|
||||
initialValues: data,
|
||||
validate: {
|
||||
@@ -83,15 +84,15 @@ const definition = defineWidget({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return 'Length must be between 1 and 100';
|
||||
return t('item.validation.length100');
|
||||
},
|
||||
href: (value) => {
|
||||
if (!z.string().min(1).max(200).safeParse(value).success) {
|
||||
return 'Length must be between 1 and 200';
|
||||
return t('item.validation.length200');
|
||||
}
|
||||
|
||||
if (!z.string().url().safeParse(value).success) {
|
||||
return 'Not a valid link';
|
||||
return t('item.validation.invalidLink');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -101,7 +102,7 @@ const definition = defineWidget({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return 'Length must be between 1 and 100';
|
||||
return t('item.validation.length400');
|
||||
},
|
||||
},
|
||||
validateInputOnChange: true,
|
||||
@@ -122,13 +123,13 @@ const definition = defineWidget({
|
||||
<TextInput
|
||||
icon={<IconTypography size="1rem" />}
|
||||
{...form.getInputProps('name')}
|
||||
label="Name"
|
||||
label={t('item.name')}
|
||||
withAsterisk
|
||||
/>
|
||||
<TextInput
|
||||
icon={<IconLink size="1rem" />}
|
||||
{...form.getInputProps('href')}
|
||||
label="URL"
|
||||
label={t('item.url')}
|
||||
withAsterisk
|
||||
/>
|
||||
<IconSelector
|
||||
@@ -140,17 +141,17 @@ const definition = defineWidget({
|
||||
/>
|
||||
<Switch
|
||||
{...form.getInputProps('openNewTab')}
|
||||
label="Open in new tab"
|
||||
label={t('item.newTab')}
|
||||
checked={form.values.openNewTab}
|
||||
/>
|
||||
<Switch
|
||||
{...form.getInputProps('hideHostname')}
|
||||
label="Hide Hostname"
|
||||
label={t('item.hideHostname')}
|
||||
checked={form.values.hideHostname}
|
||||
/>
|
||||
<Switch
|
||||
{...form.getInputProps('hideIcon')}
|
||||
label="Hide Icon"
|
||||
label={t('item.hideIcon')}
|
||||
checked={form.values.hideIcon}
|
||||
/>
|
||||
<Button
|
||||
@@ -159,11 +160,11 @@ const definition = defineWidget({
|
||||
variant="light"
|
||||
type="button"
|
||||
>
|
||||
Delete
|
||||
{t('item.delete')}
|
||||
</Button>
|
||||
{!form.isValid() && (
|
||||
<Alert color="red" icon={<IconAlertTriangle size="1rem" />}>
|
||||
Did not save, because there were validation errors. Please adust your inputs
|
||||
{t('item.validation.errorMsg')}
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -174,18 +175,9 @@ const definition = defineWidget({
|
||||
layout: {
|
||||
type: 'select',
|
||||
data: [
|
||||
{
|
||||
label: 'Auto Grid',
|
||||
value: 'autoGrid',
|
||||
},
|
||||
{
|
||||
label: 'Horizontal',
|
||||
value: 'horizontal',
|
||||
},
|
||||
{
|
||||
label: 'Vertical',
|
||||
value: 'vertical',
|
||||
},
|
||||
{ value: 'autoGrid', },
|
||||
{ value: 'horizontal', },
|
||||
{ value: 'vertical', },
|
||||
],
|
||||
defaultValue: 'autoGrid',
|
||||
},
|
||||
@@ -206,10 +198,10 @@ interface BookmarkWidgetTileProps {
|
||||
}
|
||||
|
||||
function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
|
||||
const { t } = useTranslation('modules/bookmark');
|
||||
const { classes } = useStyles();
|
||||
const { enabled: isEditModeEnabled } = useEditModeStore();
|
||||
const { fn, colors, colorScheme } = useMantineTheme();
|
||||
const { t } = useTranslation('modules/bookmark');
|
||||
|
||||
if (widget.properties.items.length === 0) {
|
||||
return (
|
||||
|
||||
@@ -2,8 +2,9 @@ import { useMantineTheme } from '@mantine/core';
|
||||
import { Calendar } from '@mantine/dates';
|
||||
import { IconCalendarTime } from '@tabler/icons-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { i18n } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getLanguageByCode } from '~/tools/language';
|
||||
import { RouterOutputs, api } from '~/utils/api';
|
||||
|
||||
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
|
||||
@@ -30,20 +31,20 @@ const definition = defineWidget({
|
||||
type: 'select',
|
||||
defaultValue: 'inCinemas',
|
||||
data: [
|
||||
{ label: 'In Cinemas', value: 'inCinemas' },
|
||||
{ label: 'Physical', value: 'physicalRelease' },
|
||||
{ label: 'Digital', value: 'digitalRelease' },
|
||||
{ value: 'inCinemas' },
|
||||
{ value: 'physicalRelease' },
|
||||
{ value: 'digitalRelease' },
|
||||
],
|
||||
},
|
||||
fontSize: {
|
||||
type: 'select',
|
||||
defaultValue: 'xs',
|
||||
data: [
|
||||
{ label: 'Extra Small', value: 'xs' },
|
||||
{ label: 'Small', value: 'sm' },
|
||||
{ label: 'Medium', value: 'md' },
|
||||
{ label: 'Large', value: 'lg' },
|
||||
{ label: 'Extra Large', value: 'xl' },
|
||||
{ value: 'xs' },
|
||||
{ value: 'sm' },
|
||||
{ value: 'md' },
|
||||
{ value: 'lg' },
|
||||
{ value: 'xl' },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -63,6 +64,7 @@ interface CalendarTileProps {
|
||||
}
|
||||
|
||||
function CalendarTile({ widget }: CalendarTileProps) {
|
||||
const { locale } = useRouter();
|
||||
const { colorScheme, radius } = useMantineTheme();
|
||||
const { name: configName } = useConfigContext();
|
||||
const [month, setMonth] = useState(new Date());
|
||||
@@ -72,6 +74,9 @@ function CalendarTile({ widget }: CalendarTileProps) {
|
||||
enabled: !!sessionData?.user,
|
||||
});
|
||||
|
||||
const language = getLanguageByCode(locale ?? 'en');
|
||||
require(`dayjs/locale/${language.locale}.js`);
|
||||
|
||||
const { data: medias } = api.calendar.medias.useQuery(
|
||||
{
|
||||
configName: configName!,
|
||||
@@ -93,7 +98,7 @@ function CalendarTile({ widget }: CalendarTileProps) {
|
||||
onPreviousMonth={setMonth}
|
||||
onNextMonth={setMonth}
|
||||
size={widget.properties.fontSize}
|
||||
locale={i18n?.resolvedLanguage ?? 'en'}
|
||||
locale={language.locale}
|
||||
firstDayOfWeek={getFirstDayOfWeek(firstDayOfWeek)}
|
||||
hideWeekdays={widget.properties.hideWeekDays}
|
||||
style={{ position: 'relative' }}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Stack, Text, createStyles } from '@mantine/core';
|
||||
import { useElementSize } from '@mantine/hooks';
|
||||
import { IconClock } from '@tabler/icons-react';
|
||||
import moment from 'moment-timezone';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { getLanguageByCode } from '~/tools/language';
|
||||
import { api } from '~/utils/api';
|
||||
import dayjs from 'dayjs';
|
||||
import timezones from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
|
||||
import { useSetSafeInterval } from '../../hooks/useSetSafeInterval';
|
||||
import { defineWidget } from '../helper';
|
||||
import { IWidget } from '../widgets';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezones);
|
||||
|
||||
const definition = defineWidget({
|
||||
id: 'date',
|
||||
icon: IconClock,
|
||||
@@ -24,14 +29,14 @@ const definition = defineWidget({
|
||||
defaultValue: 'dddd, MMMM D',
|
||||
data: () => [
|
||||
{ value: 'hide' },
|
||||
{ value: 'dddd, MMMM D', label: moment().format('dddd, MMMM D') },
|
||||
{ value: 'dddd, D MMMM', label: moment().format('dddd, D MMMM') },
|
||||
{ value: 'MMM D', label: moment().format('MMM D') },
|
||||
{ value: 'D MMM', label: moment().format('D MMM') },
|
||||
{ value: 'DD/MM/YYYY', label: moment().format('DD/MM/YYYY') },
|
||||
{ value: 'MM/DD/YYYY', label: moment().format('MM/DD/YYYY') },
|
||||
{ value: 'DD/MM', label: moment().format('DD/MM') },
|
||||
{ value: 'MM/DD', label: moment().format('MM/DD') },
|
||||
{ value: 'dddd, MMMM D', label: dayjs().format('dddd, MMMM D') },
|
||||
{ value: 'dddd, D MMMM', label: dayjs().format('dddd, D MMMM') },
|
||||
{ value: 'MMM D', label: dayjs().format('MMM D') },
|
||||
{ value: 'D MMM', label: dayjs().format('D MMM') },
|
||||
{ value: 'DD/MM/YYYY', label: dayjs().format('DD/MM/YYYY') },
|
||||
{ value: 'MM/DD/YYYY', label: dayjs().format('MM/DD/YYYY') },
|
||||
{ value: 'DD/MM', label: dayjs().format('DD/MM') },
|
||||
{ value: 'MM/DD', label: dayjs().format('MM/DD') },
|
||||
],
|
||||
},
|
||||
enableTimezone: {
|
||||
@@ -84,11 +89,11 @@ function DateTile({ widget }: DateTileProps) {
|
||||
className={cx(classes.extras, 'dashboard-tile-clock-city')}
|
||||
>
|
||||
{widget.properties.timezoneLocation.name}
|
||||
{widget.properties.titleState === 'both' && moment(date).format(' (z)')}
|
||||
{widget.properties.titleState === 'both' && dayjs(date).format(' (z)')}
|
||||
</Text>
|
||||
)}
|
||||
<Text className={cx(classes.clock, 'dashboard-tile-clock-hour')}>
|
||||
{moment(date).format(formatString)}
|
||||
{dayjs(date).format(formatString)}
|
||||
</Text>
|
||||
{!widget.properties.dateFormat.includes('hide') && (
|
||||
<Text
|
||||
@@ -96,7 +101,7 @@ function DateTile({ widget }: DateTileProps) {
|
||||
pt="0.2rem"
|
||||
className={cx(classes.extras, 'dashboard-tile-clock-date')}
|
||||
>
|
||||
{moment(date).format(widget.properties.dateFormat)}
|
||||
{dayjs(date).format(widget.properties.dateFormat)}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -139,7 +144,7 @@ const useDateState = (location?: { latitude: number; longitude: number }) => {
|
||||
const timeoutRef = useRef<NodeJS.Timeout>(); // reference for initial timeout until first minute change
|
||||
useEffect(() => {
|
||||
const language = getLanguageByCode(locale ?? 'en');
|
||||
moment.locale(language.momentLocale);
|
||||
dayjs.locale(language.locale);
|
||||
setDate(getNewDate(timezone));
|
||||
timeoutRef.current = setTimeout(
|
||||
() => {
|
||||
@@ -150,9 +155,8 @@ const useDateState = (location?: { latitude: number; longitude: number }) => {
|
||||
}, 1000 * 60);
|
||||
//1 minute - current seconds and milliseconds count
|
||||
},
|
||||
1000 * 60 - (1000 * moment().seconds() + moment().milliseconds())
|
||||
1000 * 60 - (1000 * dayjs().second() + dayjs().millisecond())
|
||||
);
|
||||
|
||||
return () => timeoutRef.current && clearTimeout(timeoutRef.current);
|
||||
}, [timezone, locale]);
|
||||
|
||||
@@ -162,9 +166,9 @@ const useDateState = (location?: { latitude: number; longitude: number }) => {
|
||||
//Returns a local date if no inputs or returns date from input zone
|
||||
const getNewDate = (timezone?: string) => {
|
||||
if (timezone) {
|
||||
return moment().tz(timezone);
|
||||
return dayjs().tz(timezone);
|
||||
}
|
||||
return moment();
|
||||
return dayjs();
|
||||
};
|
||||
|
||||
export default definition;
|
||||
|
||||
@@ -121,7 +121,7 @@ function IFrameTile({ widget }: IFrameTileProps) {
|
||||
title="widget iframe"
|
||||
allow={allowedPermissions.join(' ')}
|
||||
>
|
||||
<Text>Your Browser does not support iframes. Please update your browser.</Text>
|
||||
<Text>{t('card.errors.browserSupport')}</Text>
|
||||
</iframe>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -59,12 +59,13 @@ const useMediaRequestDecisionMutation = () => {
|
||||
utils.mediaRequest.all.invalidate();
|
||||
},
|
||||
});
|
||||
const { t } = useTranslation('modules/media-requests-list');
|
||||
return async (variables: MediaRequestDecisionVariables) => {
|
||||
const action = variables.isApproved ? 'Approving' : 'Declining';
|
||||
const action = variables.isApproved ? t('mutation.approving') : t('mutation.declining');
|
||||
notifications.show({
|
||||
id: `decide-${variables.request.id}`,
|
||||
color: 'yellow',
|
||||
title: `${action} request...`,
|
||||
title: `${action} ${t('mutation.request')}`,
|
||||
message: undefined,
|
||||
loading: true,
|
||||
});
|
||||
@@ -76,7 +77,7 @@ const useMediaRequestDecisionMutation = () => {
|
||||
},
|
||||
{
|
||||
onSuccess(_data, variables) {
|
||||
const title = variables.isApproved ? 'Request was approved!' : 'Request was declined!';
|
||||
const title = variables.isApproved ? t('mutation.approved') : t('mutation.declined');
|
||||
notifications.update({
|
||||
id: `decide-${variables.id}`,
|
||||
color: 'teal',
|
||||
@@ -189,7 +190,7 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
|
||||
notifications.show({
|
||||
id: `approve ${item.id}`,
|
||||
color: 'yellow',
|
||||
title: 'Approving request...',
|
||||
title: t('tooltips.approving'),
|
||||
message: undefined,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
@@ -16,8 +16,8 @@ const definition = defineWidget({
|
||||
type: 'select',
|
||||
defaultValue: 'row' as 'row' | 'column',
|
||||
data: [
|
||||
{ label: 'Horizontal', value: 'row' },
|
||||
{ label: 'Vertical', value: 'column' },
|
||||
{ value: 'row' },
|
||||
{ value: 'column' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,27 +2,29 @@ import { Card, Divider, Flex, Grid, Group, Text } from '@mantine/core';
|
||||
import { IconDeviceMobile, IconId } from '@tabler/icons-react';
|
||||
|
||||
import { GenericSessionInfo } from '../../types/api/media-server/session-info';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const DetailCollapseable = ({ session }: { session: GenericSessionInfo }) => {
|
||||
let details: { title: string; metrics: { name: string; value: string | undefined }[] }[] = [];
|
||||
const { t } = useTranslation('modules/media-server-list');
|
||||
|
||||
if (session.currentlyPlaying) {
|
||||
if (session.currentlyPlaying.metadata.video) {
|
||||
details = [
|
||||
...details,
|
||||
{
|
||||
title: 'Video',
|
||||
title: t('detail.video.'),
|
||||
metrics: [
|
||||
{
|
||||
name: 'Resolution',
|
||||
name: t('detail.video.resolution'),
|
||||
value: `${session.currentlyPlaying.metadata.video.width}x${session.currentlyPlaying.metadata.video.height}`,
|
||||
},
|
||||
{
|
||||
name: 'Framerate',
|
||||
name: t('detail.video.framerate'),
|
||||
value: session.currentlyPlaying.metadata.video.videoFrameRate,
|
||||
},
|
||||
{
|
||||
name: 'Codec',
|
||||
name: t('detail.video.codec'),
|
||||
value: session.currentlyPlaying.metadata.video.videoCodec,
|
||||
},
|
||||
{
|
||||
@@ -39,14 +41,14 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
|
||||
details = [
|
||||
...details,
|
||||
{
|
||||
title: 'Audio',
|
||||
title: t('detail.audio.audio'),
|
||||
metrics: [
|
||||
{
|
||||
name: 'Audio channels',
|
||||
name: t('detail.audio.channels'),
|
||||
value: `${session.currentlyPlaying.metadata.audio.audioChannels}`,
|
||||
},
|
||||
{
|
||||
name: 'Audio codec',
|
||||
name: t('detail.audio.codec'),
|
||||
value: session.currentlyPlaying.metadata.audio.audioCodec,
|
||||
},
|
||||
],
|
||||
@@ -58,24 +60,24 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
|
||||
details = [
|
||||
...details,
|
||||
{
|
||||
title: 'Transcoding',
|
||||
title: t('detail.transcoding.transcoding'),
|
||||
metrics: [
|
||||
{
|
||||
name: 'Resolution',
|
||||
name: t('detail.video.resolution'),
|
||||
value: `${session.currentlyPlaying.metadata.transcoding.width}x${session.currentlyPlaying.metadata.transcoding.height}`,
|
||||
},
|
||||
{
|
||||
name: 'Context',
|
||||
name: t('detail.transcoding.context'),
|
||||
value: session.currentlyPlaying.metadata.transcoding.context,
|
||||
},
|
||||
{
|
||||
name: 'Hardware encoding requested',
|
||||
name: t('detail.transcoding.requested'),
|
||||
value: session.currentlyPlaying.metadata.transcoding.transcodeHwRequested
|
||||
? 'yes'
|
||||
: 'no',
|
||||
},
|
||||
{
|
||||
name: 'Source codec',
|
||||
name: t('detail.transcoding.source'),
|
||||
value:
|
||||
session.currentlyPlaying.metadata.transcoding.sourceAudioCodec ||
|
||||
session.currentlyPlaying.metadata.transcoding.sourceVideoCodec
|
||||
@@ -83,7 +85,7 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
name: 'Target codec',
|
||||
name: t('detail.transcoding.target'),
|
||||
value: `${session.currentlyPlaying.metadata.transcoding.videoCodec} ${session.currentlyPlaying.metadata.transcoding.audioCodec}`,
|
||||
},
|
||||
],
|
||||
@@ -97,19 +99,19 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
|
||||
<Flex justify="space-between" mb="xs">
|
||||
<Group>
|
||||
<IconId size={16} />
|
||||
<Text>ID</Text>
|
||||
<Text>{t('detail.id')}</Text>
|
||||
</Group>
|
||||
<Text>{session.id}</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" mb="md">
|
||||
<Group>
|
||||
<IconDeviceMobile size={16} />
|
||||
<Text>Device</Text>
|
||||
<Text>{t('detail.device')}</Text>
|
||||
</Group>
|
||||
<Text>{session.sessionName}</Text>
|
||||
</Flex>
|
||||
{details.length > 0 && (
|
||||
<Divider label="Stats for nerds" labelPosition="center" mt="lg" mb="sm" />
|
||||
<Divider label={t('detail.label')} labelPosition="center" mt="lg" mb="sm" />
|
||||
)}
|
||||
<Grid>
|
||||
{details.map((detail, index) => (
|
||||
|
||||
@@ -42,7 +42,6 @@ interface MediaServerWidgetProps {
|
||||
function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
||||
const { t } = useTranslation('modules/media-server');
|
||||
const { config } = useConfigContext();
|
||||
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||
|
||||
const { data, isError, isFetching, isInitialLoading } = useGetMediaServers({
|
||||
enabled: config !== undefined,
|
||||
@@ -72,7 +71,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
|
||||
<Loader />
|
||||
<Stack align="center" spacing={0}>
|
||||
<Text>{t('descriptor.name')}</Text>
|
||||
<Text color="dimmed">Homarr is loading streams...</Text>
|
||||
<Text color="dimmed">{t('descriptor.loading')}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Icon,
|
||||
IconDeviceTv,
|
||||
IconHeadphones,
|
||||
IconMovie,
|
||||
IconQuestionMark,
|
||||
IconVideo,
|
||||
} from '@tabler/icons-react';
|
||||
@@ -23,6 +24,8 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
|
||||
return IconHeadphones;
|
||||
case 'tv':
|
||||
return IconDeviceTv;
|
||||
case 'movie':
|
||||
return IconMovie;
|
||||
case 'video':
|
||||
return IconVideo;
|
||||
default:
|
||||
|
||||
@@ -42,6 +42,7 @@ const definition = defineWidget({
|
||||
dangerousAllowSanitizedItemContent: {
|
||||
type: 'switch',
|
||||
defaultValue: false,
|
||||
info: true,
|
||||
},
|
||||
textLinesClamp: {
|
||||
type: 'slider',
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
createStyles,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure, useElementSize } from '@mantine/hooks';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import {
|
||||
IconAffiliate,
|
||||
IconDatabase,
|
||||
@@ -37,9 +37,8 @@ interface TorrentQueueItemProps {
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const BitTorrrentQueueItem = ({ torrent, width, app }: TorrentQueueItemProps) => {
|
||||
export const BitTorrentQueueItem = ({ torrent, width, app }: TorrentQueueItemProps) => {
|
||||
const [popoverOpened, { open: openPopover, close: closePopover }] = useDisclosure(false);
|
||||
const theme = useMantineTheme();
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation('modules/torrents-status');
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { AppIntegrationType } from '../../types/app';
|
||||
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
|
||||
import { defineWidget } from '../helper';
|
||||
import { IWidget } from '../widgets';
|
||||
import { BitTorrrentQueueItem } from './TorrentQueueItem';
|
||||
import { BitTorrentQueueItem } from './TorrentQueueItem';
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
@@ -108,7 +108,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
<Loader />
|
||||
<Stack align="center" spacing={0}>
|
||||
<Text>{t('card.loading.title')}</Text>
|
||||
<Text color="dimmed">Homarr is establishing a connection...</Text>
|
||||
<Text color="dimmed">{t('card.loading.description')}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
@@ -156,7 +156,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredTorrents.map((torrent, index) => (
|
||||
<BitTorrrentQueueItem key={index} torrent={torrent} width={width} app={undefined} />
|
||||
<BitTorrentQueueItem key={index} torrent={torrent} width={width} app={undefined} />
|
||||
))}
|
||||
|
||||
{filteredTorrents.length !== torrents.length && (
|
||||
|
||||
@@ -21,8 +21,8 @@ import duration from 'dayjs/plugin/duration';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
|
||||
import { useGetUsenetDownloads } from '../dashDot/api';
|
||||
import { humanFileSize } from '../../tools/humanFileSize';
|
||||
import { useGetUsenetDownloads } from '../dashDot/api';
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
@@ -91,7 +91,6 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
|
||||
<Table highlightOnHover style={{ tableLayout: 'fixed' }} ref={ref}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 32 }} />
|
||||
<th style={{ width: '75%' }}>{t('queue.header.name')}</th>
|
||||
{sizeBreakpoint < width ? (
|
||||
<th style={{ width: 100 }}>{t('queue.header.size')}</th>
|
||||
@@ -107,21 +106,6 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
|
||||
<tbody>
|
||||
{data.items.map((nzb) => (
|
||||
<tr key={nzb.id}>
|
||||
<td>
|
||||
{nzb.state === 'paused' ? (
|
||||
<Tooltip label="NOT IMPLEMENTED">
|
||||
<ActionIcon color="gray" variant="subtle" radius="xl" size="sm">
|
||||
<IconPlayerPlay size="16" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip label="NOT IMPLEMENTED">
|
||||
<ActionIcon color="primary" variant="subtle" radius="xl" size="sm">
|
||||
<IconPlayerPause size="16" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Tooltip position="top" label={nzb.name}>
|
||||
<Text
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
IconSun,
|
||||
} from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useElementSize } from '@mantine/hooks';
|
||||
|
||||
interface WeatherIconProps {
|
||||
code: number;
|
||||
@@ -25,8 +24,7 @@ interface WeatherIconProps {
|
||||
*/
|
||||
export const WeatherIcon = ({ code, size=50 }: WeatherIconProps) => {
|
||||
const { t } = useTranslation('modules/weather');
|
||||
const { width, ref } = useElementSize();
|
||||
|
||||
|
||||
const { icon: Icon, name } =
|
||||
weatherDefinitions.find((wd) => wd.codes.includes(code)) ?? unknownWeather;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
IconArrowDownRight,
|
||||
IconArrowUpRight,
|
||||
IconCloudRain,
|
||||
IconCurrentLocation,
|
||||
IconMapPin,
|
||||
} from '@tabler/icons-react';
|
||||
import { api } from '~/utils/api';
|
||||
@@ -12,6 +11,7 @@ import { api } from '~/utils/api';
|
||||
import { defineWidget } from '../helper';
|
||||
import { IWidget } from '../widgets';
|
||||
import { WeatherIcon } from './WeatherIcon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const definition = defineWidget({
|
||||
id: 'weather',
|
||||
@@ -52,6 +52,7 @@ interface WeatherTileProps {
|
||||
function WeatherTile({ widget }: WeatherTileProps) {
|
||||
const { data: weather, isLoading, isError } = api.weather.at.useQuery(widget.properties.location);
|
||||
const { width, ref } = useElementSize();
|
||||
const { t } = useTranslation('modules/weather');
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -77,7 +78,7 @@ function WeatherTile({ widget }: WeatherTileProps) {
|
||||
if (isError) {
|
||||
return (
|
||||
<Center>
|
||||
<Text weight={500}>An error occured</Text>
|
||||
<Text weight={500}>{t('error')}</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user