🔀 Merge branch 'dev' into feature/add-basic-authentication

This commit is contained in:
Manuel
2023-08-23 21:17:43 +02:00
42 changed files with 971 additions and 770 deletions

View File

@@ -86,7 +86,7 @@ jobs:
# generate Docker tags based on the following events/attributes # generate Docker tags based on the following events/attributes
tags: | tags: |
type=ref,event=pr 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 == '' }} tpye=raw,value=dev,priority=1,enable=${{ github.event.inputs.tag == '' }}
- name: Set up QEMU - name: Set up QEMU
@@ -114,43 +114,3 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max 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

View File

@@ -30,6 +30,7 @@ module.exports = {
'tr', 'tr',
'lv', 'lv',
'hr', 'hr',
'hu'
], ],
localeDetection: true, localeDetection: true,

View File

@@ -84,7 +84,7 @@
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"moment": "^2.29.4", "moment": "^2.29.4",
"moment-timezone": "^0.5.43", "moment-timezone": "^0.5.43",
"next": "13.4.10", "next": "13.4.19",
"next-auth": "^4.22.3", "next-auth": "^4.22.3",
"next-i18next": "^14.0.0", "next-i18next": "^14.0.0",
"nzbget-api": "^0.0.3", "nzbget-api": "^0.0.3",
@@ -111,7 +111,7 @@
"@trivago/prettier-plugin-sort-imports": "^4.2.0", "@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/cookies": "^0.7.7", "@types/cookies": "^0.7.7",
"@types/dockerode": "^3.3.9", "@types/dockerode": "^3.3.9",
"@types/node": "18.16.19", "@types/node": "18.17.8",
"@types/prismjs": "^1.26.0", "@types/prismjs": "^1.26.0",
"@types/react": "^18.2.11", "@types/react": "^18.2.11",
"@types/uuid": "^9.0.0", "@types/uuid": "^9.0.0",

View File

@@ -14,5 +14,12 @@
"category": { "category": {
"openAllInNewTab": "Open all in new tab" "openAllInNewTab": "Open all in new tab"
} }
},
"menu": {
"moveUp": "Move up",
"moveDown": "Move down",
"addCategory": "Add category",
"addAbove": "above",
"addBelow": "below"
} }
} }

View File

@@ -7,5 +7,19 @@
"goBack": "Go back to the previous step", "goBack": "Go back to the previous step",
"actionIcon": { "actionIcon": {
"tooltip": "Add a tile" "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"
}
} }
} }

View File

@@ -6,6 +6,12 @@
"key": "Shortcut key", "key": "Shortcut key",
"action": "Action", "action": "Action",
"keybinds": "Keybinds", "keybinds": "Keybinds",
"actions": {
"toggleTheme": "Toggle light/dark mode",
"focusSearchBar": "Focus on search bar",
"openDocker": "Open docker Widget",
"toggleEdit": "Toggle Edit Mode"
},
"metrics": { "metrics": {
"configurationSchemaVersion": "Configuration schema version", "configurationSchemaVersion": "Configuration schema version",
"configurationsCount": "Available configurations", "configurationsCount": "Available configurations",
@@ -15,4 +21,5 @@
"locales": "Configured I18n locales", "locales": "Configured I18n locales",
"experimental_disableEditMode": "<b>EXPERIMENTAL</b>: Disable edit mode" "experimental_disableEditMode": "<b>EXPERIMENTAL</b>: Disable edit mode"
} }
} }

View File

@@ -99,6 +99,12 @@
} }
}, },
"validation": { "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"
} }
} }

View File

@@ -12,7 +12,12 @@
"label": "Items" "label": "Items"
}, },
"layout": { "layout": {
"label": "Layout" "label": "Layout",
"data":{
"autoGrid": "Auto Grid",
"horizontal": "Horizontal",
"vertical": "Vertical"
}
} }
} }
}, },
@@ -21,5 +26,21 @@
"title": "Bookmark list empty", "title": "Bookmark list empty",
"text": "Add new items to this list in the edit mode" "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"
} }
} }

View File

@@ -8,13 +8,25 @@
"label": "Use Sonarr v4 API" "label": "Use Sonarr v4 API"
}, },
"radarrReleaseType": { "radarrReleaseType": {
"label": "Radarr release type" "label": "Radarr release type",
"data":{
"inCinemas": "In Cinemas",
"physicalRelease": "Physical",
"digitalRelease": "Digital"
}
}, },
"hideWeekDays": { "hideWeekDays": {
"label": "Hide week days" "label": "Hide week days"
}, },
"fontSize": { "fontSize": {
"label": "Font Size" "label": "Font Size",
"data":{
"xs": "Extra Small",
"sm": "Small",
"md": "Medium",
"lg": "Large",
"xl": "Extra Large"
}
} }
} }
} }

View File

@@ -38,7 +38,8 @@
"noUrl": { "noUrl": {
"title": "Invalid URL", "title": "Invalid URL",
"text": "Ensure that you've entered a valid address in the configuration of your widget" "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."
} }
} }
} }

View File

@@ -19,6 +19,37 @@
}, },
"tooltips": { "tooltips": {
"approve": "Approve requests", "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"
}
} }
} }

View File

@@ -5,7 +5,11 @@
"settings": { "settings": {
"title": "Media requests stats", "title": "Media requests stats",
"direction": { "direction": {
"label": "Direction of the layout." "label": "Direction of the layout.",
"data":{
"row": "Horizontal",
"column": "Vertical"
}
} }
} }
}, },

View File

@@ -6,6 +6,7 @@
"title": "Settings for media server widget" "title": "Settings for media server widget"
} }
}, },
"loading": "Loading streams",
"card": { "card": {
"table": { "table": {
"header": { "header": {

View File

@@ -12,7 +12,8 @@
"label": "Refresh interval (in minutes)" "label": "Refresh interval (in minutes)"
}, },
"dangerousAllowSanitizedItemContent": { "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": { "textLinesClamp": {
"label": "Text lines clamp" "label": "Text lines clamp"

View File

@@ -59,11 +59,12 @@
}, },
"generic": { "generic": {
"title": "An unexpected error occurred", "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": { "loading": {
"title": "Loading..." "title": "Loading",
"description": "Establishing a connection"
}, },
"popover": { "popover": {
"introductionPrefix": "Managed by", "introductionPrefix": "Managed by",

View File

@@ -32,5 +32,6 @@
"thunderstormWithHail": "Thunderstorm with hail", "thunderstormWithHail": "Thunderstorm with hail",
"unknown": "Unknown" "unknown": "Unknown"
} }
} },
"error": "An error occured"
} }

View File

@@ -47,14 +47,14 @@ export const EditAppModal = ({
const form = useForm<AppType>({ const form = useForm<AppType>({
initialValues: innerProps.app, initialValues: innerProps.app,
validate: { validate: {
name: (name) => (!name ? 'Name is required' : null), name: (name) => (!name ? t('validation.name') : null),
url: (url) => { url: (url) => {
if (!url) { if (!url) {
return 'Url is required'; return t('validation.noUrl');
} }
if (!url.match(appUrlRegex)) { if (!url.match(appUrlRegex)) {
return 'Value is not a valid url'; return t('validation.invalidUrl');
} }
return null; return null;
@@ -62,7 +62,7 @@ export const EditAppModal = ({
appearance: { appearance: {
iconUrl: (url: string) => { iconUrl: (url: string) => {
if (url.length < 1) { if (url.length < 1) {
return 'This field is required'; return t('validation.noIconUrl');
} }
return null; return null;
@@ -71,11 +71,11 @@ export const EditAppModal = ({
behaviour: { behaviour: {
externalUrl: (url: string) => { externalUrl: (url: string) => {
if (url === undefined || url.length < 1) { if (url === undefined || url.length < 1) {
return 'External URI is required'; return t('validation.noExternalUri');
} }
if (!url.match(appUrlWithAnyProtocolRegex)) { if (!url.match(appUrlWithAnyProtocolRegex)) {
return 'External URI is not a valid uri'; return t('validation.invalidExternalUri');
} }
return null; return null;

View File

@@ -33,12 +33,12 @@ export const AvailableElementTypes = ({
const onClickCreateCategory = async () => { const onClickCreateCategory = async () => {
openContextModalGeneric<CategoryEditModalInnerProps>({ openContextModalGeneric<CategoryEditModalInnerProps>({
modal: 'categoryEditModal', modal: 'categoryEditModal',
title: 'Name of new category', title: t('category.newName'),
withCloseButton: false, withCloseButton: false,
innerProps: { innerProps: {
category: { category: {
id: uuidv4(), id: uuidv4(),
name: 'New category', name: t('category.defaultName'),
position: 0, // doesn't matter, is being overwritten position: 0, // doesn't matter, is being overwritten
}, },
onSuccess: async (category) => { onSuccess: async (category) => {
@@ -65,8 +65,8 @@ export const AvailableElementTypes = ({
})).then(() => { })).then(() => {
closeModal(modalId); closeModal(modalId);
showNotification({ showNotification({
title: 'Category created', title: t('category.created.title'),
message: `The category ${category.name} has been created`, message: t('category.created.message', { name: category.name}),
color: 'teal', color: 'teal',
}); });
}); });
@@ -81,7 +81,7 @@ export const AvailableElementTypes = ({
<Space h="lg" /> <Space h="lg" />
<Group spacing="md" grow> <Group spacing="md" grow>
<ElementItem <ElementItem
name="Apps" name={t('apps')}
icon={<IconBox size={40} strokeWidth={1.3} />} icon={<IconBox size={40} strokeWidth={1.3} />}
onClick={() => { onClick={() => {
openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({ openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({
@@ -89,7 +89,7 @@ export const AvailableElementTypes = ({
innerProps: { innerProps: {
app: { app: {
id: uuidv4(), id: uuidv4(),
name: 'Your app', name: t('app.defaultName'),
url: 'https://homarr.dev', url: 'https://homarr.dev',
appearance: { appearance: {
iconUrl: '/imgs/logo/logo.png', iconUrl: '/imgs/logo/logo.png',
@@ -126,12 +126,12 @@ export const AvailableElementTypes = ({
}} }}
/> />
<ElementItem <ElementItem
name="Widgets" name={t('widgets')}
icon={<IconStack size={40} strokeWidth={1.3} />} icon={<IconStack size={40} strokeWidth={1.3} />}
onClick={onOpenWidgets} onClick={onOpenWidgets}
/> />
<ElementItem <ElementItem
name="Category" name={t('categories')}
icon={<IconBoxAlignTop size={40} strokeWidth={1.3} />} icon={<IconBoxAlignTop size={40} strokeWidth={1.3} />}
onClick={onClickCreateCategory} onClick={onClickCreateCategory}
/> />

View File

@@ -12,6 +12,7 @@ import {
import { useConfigContext } from '../../../../config/provider'; import { useConfigContext } from '../../../../config/provider';
import { CategoryType } from '../../../../types/category'; import { CategoryType } from '../../../../types/category';
import { useCategoryActions } from './useCategoryActions'; import { useCategoryActions } from './useCategoryActions';
import { useTranslation } from 'next-i18next';
interface CategoryEditMenuProps { interface CategoryEditMenuProps {
category: CategoryType; category: CategoryType;
@@ -21,6 +22,7 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
const { name: configName } = useConfigContext(); const { name: configName } = useConfigContext();
const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit, remove } = const { addCategoryAbove, addCategoryBelow, moveCategoryUp, moveCategoryDown, edit, remove } =
useCategoryActions(configName, category); useCategoryActions(configName, category);
const { t } = useTranslation(['layout/common','common']);
return ( return (
<Menu withinPortal withArrow> <Menu withinPortal withArrow>
@@ -31,24 +33,28 @@ export const CategoryEditMenu = ({ category }: CategoryEditMenuProps) => {
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
<Menu.Item icon={<IconEdit size={20} />} onClick={edit}> <Menu.Item icon={<IconEdit size={20} />} onClick={edit}>
Edit {t('common:edit')}
</Menu.Item> </Menu.Item>
<Menu.Item icon={<IconTrash size={20} />} onClick={remove}> <Menu.Item icon={<IconTrash size={20} />} onClick={remove}>
Remove {t('common:remove')}
</Menu.Item> </Menu.Item>
<Menu.Label>Change positon</Menu.Label> <Menu.Label>
{t('common:changePosition')}
</Menu.Label>
<Menu.Item icon={<IconTransitionTop size={20} />} onClick={moveCategoryUp}> <Menu.Item icon={<IconTransitionTop size={20} />} onClick={moveCategoryUp}>
Move up {t('menu.moveUp')}
</Menu.Item> </Menu.Item>
<Menu.Item icon={<IconTransitionBottom size={20} />} onClick={moveCategoryDown}> <Menu.Item icon={<IconTransitionBottom size={20} />} onClick={moveCategoryDown}>
Move down {t('menu.moveDown')}
</Menu.Item> </Menu.Item>
<Menu.Label>Add category</Menu.Label> <Menu.Label>
{t('menu.addCategory')}
</Menu.Label>
<Menu.Item icon={<IconRowInsertTop size={20} />} onClick={addCategoryAbove}> <Menu.Item icon={<IconRowInsertTop size={20} />} onClick={addCategoryAbove}>
Add category above {t('menu.addCategory') + ' ' + t('menu.addAbove')}
</Menu.Item> </Menu.Item>
<Menu.Item icon={<IconRowInsertBottom size={20} />} onClick={addCategoryBelow}> <Menu.Item icon={<IconRowInsertBottom size={20} />} onClick={addCategoryBelow}>
Add category below {t('menu.addCategory') + ' ' + t('menu.addBelow')}
</Menu.Item> </Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>

View File

@@ -53,10 +53,10 @@ export const AboutModal = ({ opened, closeModal, newVersionAvailable }: AboutMod
const { t } = useTranslation(['common', 'layout/modals/about']); const { t } = useTranslation(['common', 'layout/modals/about']);
const keybinds = [ const keybinds = [
{ key: 'Mod + J', shortcut: 'Toggle light/dark mode' }, { key: 'Mod + J', shortcut: t('layout/modals/about:actions.toggleTheme') },
{ key: 'Mod + K', shortcut: 'Focus on search bar' }, { key: 'Mod + K', shortcut: t('layout/modals/about:actions.focusSearchBar') },
{ key: 'Mod + B', shortcut: 'Open docker widget' }, { key: 'Mod + B', shortcut: t('layout/modals/about:actions.openDocker') },
{ key: 'Mod + E', shortcut: 'Toggle Edit mode' }, { key: 'Mod + E', shortcut: t('layout/modals/about:actions.toggleEdit') },
]; ];
const rows = keybinds.map((element) => ( const rows = keybinds.map((element) => (
<tr key={element.key}> <tr key={element.key}>

View File

@@ -49,6 +49,6 @@ export async function getStaticProps({ req, res, locale }: GetServerSidePropsCon
const useStyles = createStyles(() => ({ const useStyles = createStyles(() => ({
image: { image: {
margin: '0 auto', margin: '0 auto',
display: 'blcok', display: 'block',
}, },
})); }));

View File

@@ -4,8 +4,10 @@ import { Notifications } from '@mantine/notifications';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import Consola from 'consola'; import Consola from 'consola';
import { getCookie, setCookie } from 'cookies-next'; 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 'flag-icons/css/flag-icons.min.css';
import moment from 'moment-timezone';
import { GetServerSidePropsContext } from 'next'; import { GetServerSidePropsContext } from 'next';
import { Session } from 'next-auth'; import { Session } from 'next-auth';
import { SessionProvider, getSession } from 'next-auth/react'; import { SessionProvider, getSession } from 'next-auth/react';
@@ -34,6 +36,9 @@ import {
} from '../tools/server/getPackageVersion'; } from '../tools/server/getPackageVersion';
import { theme } from '../tools/server/theme/theme'; import { theme } from '../tools/server/theme/theme';
dayjs.extend(locale);
dayjs.extend(utc);
function App( function App(
this: any, this: any,
props: AppProps<{ props: AppProps<{
@@ -53,8 +58,8 @@ function App(
const { Component, pageProps } = props; const { Component, pageProps } = props;
// TODO: make mapping from our locales to moment locales // TODO: make mapping from our locales to moment locales
const language = getLanguageByCode(pageProps.locale); const language = getLanguageByCode(pageProps.locale);
require('moment/locale/' + language.momentLocale); require(`dayjs/locale/${language.locale}.js`);
moment.locale(language.momentLocale); dayjs.locale(language.locale);
const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>( const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>(
props.pageProps.primaryColor ?? 'red' props.pageProps.primaryColor ?? 'red'

View File

@@ -12,13 +12,21 @@ export const notebookRouter = createTRPCRouter({
update: publicProcedure update: publicProcedure
.input(z.object({ widgetId: z.string(), content: z.string(), configName: z.string() })) .input(z.object({ widgetId: z.string(), content: z.string(), configName: z.string() }))
.mutation(async ({ input }) => { .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 config = getConfig(input.configName);
const widget = config.widgets.find((widget) => widget.id === input.widgetId) as const widget = config.widgets.find((widget) => widget.id === input.widgetId) as
| INotebookWidget | INotebookWidget
| undefined; | undefined;
if (!widget) { if (!widget) {
return new TRPCError({ throw new TRPCError({
code: 'BAD_REQUEST', code: 'BAD_REQUEST',
message: 'Specified widget was not found', message: 'Specified widget was not found',
}); });

View File

@@ -10,7 +10,7 @@ export type Language = {
*/ */
country?: string; country?: string;
momentLocale: string; locale: string;
}; };
export const languages: Language[] = [ export const languages: Language[] = [
@@ -20,7 +20,7 @@ export const languages: Language[] = [
translatedName: 'German', translatedName: 'German',
emoji: '🇩🇪', emoji: '🇩🇪',
country: 'DE', country: 'DE',
momentLocale: 'de', locale: 'de',
}, },
{ {
shortName: 'en', shortName: 'en',
@@ -28,7 +28,7 @@ export const languages: Language[] = [
translatedName: 'English', translatedName: 'English',
emoji: '🇬🇧', emoji: '🇬🇧',
country: 'GB', country: 'GB',
momentLocale: 'en-gb', locale: 'en-gb',
}, },
// Danish // Danish
{ {
@@ -37,7 +37,7 @@ export const languages: Language[] = [
translatedName: 'Danish', translatedName: 'Danish',
emoji: '🇩🇰', emoji: '🇩🇰',
country: 'DK', country: 'DK',
momentLocale: 'da', locale: 'da',
}, },
// Hebrew // Hebrew
{ {
@@ -46,7 +46,7 @@ export const languages: Language[] = [
translatedName: 'Hebrew', translatedName: 'Hebrew',
emoji: '🇮🇱', emoji: '🇮🇱',
country: 'IL', country: 'IL',
momentLocale: 'he', locale: 'he',
}, },
{ {
shortName: 'es', shortName: 'es',
@@ -54,7 +54,7 @@ export const languages: Language[] = [
translatedName: 'Spanish', translatedName: 'Spanish',
emoji: '🇪🇸', emoji: '🇪🇸',
country: 'ES', country: 'ES',
momentLocale: 'es', locale: 'es',
}, },
{ {
shortName: 'fr', shortName: 'fr',
@@ -62,7 +62,7 @@ export const languages: Language[] = [
translatedName: 'French', translatedName: 'French',
emoji: '🇫🇷', emoji: '🇫🇷',
country: 'FR', country: 'FR',
momentLocale: 'fr', locale: 'fr',
}, },
{ {
shortName: 'it', shortName: 'it',
@@ -70,7 +70,7 @@ export const languages: Language[] = [
translatedName: 'Italian', translatedName: 'Italian',
emoji: '🇮🇹', emoji: '🇮🇹',
country: 'IT', country: 'IT',
momentLocale: 'it', locale: 'it',
}, },
{ {
shortName: 'ja', shortName: 'ja',
@@ -78,7 +78,7 @@ export const languages: Language[] = [
translatedName: 'Japanese', translatedName: 'Japanese',
emoji: '🇯🇵', emoji: '🇯🇵',
country: 'JP', country: 'JP',
momentLocale: 'jp' locale: 'ja',
}, },
{ {
shortName: 'ko', shortName: 'ko',
@@ -86,14 +86,14 @@ export const languages: Language[] = [
translatedName: 'Korean', translatedName: 'Korean',
emoji: '🇰🇷', emoji: '🇰🇷',
country: 'KR', country: 'KR',
momentLocale: 'ko' locale: 'ko',
}, },
{ {
shortName: 'lol', shortName: 'lol',
originalName: 'LOLCAT', originalName: 'LOLCAT',
translatedName: 'LOLCAT', translatedName: 'LOLCAT',
emoji: '🐱', emoji: '🐱',
momentLocale: 'en-gb', locale: 'en-gb',
}, },
// Norwegian // Norwegian
{ {
@@ -102,7 +102,7 @@ export const languages: Language[] = [
translatedName: 'Norwegian', translatedName: 'Norwegian',
emoji: '🇳🇴', emoji: '🇳🇴',
country: 'NO', country: 'NO',
momentLocale: 'nb', locale: 'nb',
}, },
// Slovak // Slovak
{ {
@@ -111,7 +111,7 @@ export const languages: Language[] = [
translatedName: 'Slovak', translatedName: 'Slovak',
emoji: '🇸🇰', emoji: '🇸🇰',
country: 'SK', country: 'SK',
momentLocale: 'sk', locale: 'sk',
}, },
{ {
shortName: 'nl', shortName: 'nl',
@@ -119,7 +119,7 @@ export const languages: Language[] = [
translatedName: 'Dutch', translatedName: 'Dutch',
emoji: '🇳🇱', emoji: '🇳🇱',
country: 'NL', country: 'NL',
momentLocale: 'nl', locale: 'nl',
}, },
{ {
shortName: 'pl', shortName: 'pl',
@@ -127,7 +127,7 @@ export const languages: Language[] = [
translatedName: 'Polish', translatedName: 'Polish',
emoji: '🇵🇱', emoji: '🇵🇱',
country: 'PL', country: 'PL',
momentLocale: 'pl', locale: 'pl',
}, },
{ {
shortName: 'pt', shortName: 'pt',
@@ -135,7 +135,7 @@ export const languages: Language[] = [
translatedName: 'Portuguese', translatedName: 'Portuguese',
emoji: '🇵🇹', emoji: '🇵🇹',
country: 'PT', country: 'PT',
momentLocale: 'pt', locale: 'pt',
}, },
{ {
shortName: 'ru', shortName: 'ru',
@@ -143,15 +143,15 @@ export const languages: Language[] = [
translatedName: 'Russian', translatedName: 'Russian',
emoji: '🇷🇺', emoji: '🇷🇺',
country: 'RU', country: 'RU',
momentLocale: 'ru', locale: 'ru',
}, },
{ {
momentLocale: 'si',
shortName: 'sl', shortName: 'sl',
originalName: 'Slovenščina', originalName: 'Slovenščina',
translatedName: 'Slovenian', translatedName: 'Slovenian',
emoji: '🇸🇮', emoji: '🇸🇮',
country: 'SI' country: 'SI',
locale: 'sl',
}, },
{ {
shortName: 'sv', shortName: 'sv',
@@ -159,7 +159,7 @@ export const languages: Language[] = [
translatedName: 'Swedish', translatedName: 'Swedish',
emoji: '🇸🇪', emoji: '🇸🇪',
country: 'SE', country: 'SE',
momentLocale: 'sv', locale: 'sv',
}, },
{ {
shortName: 'uk', shortName: 'uk',
@@ -167,7 +167,7 @@ export const languages: Language[] = [
translatedName: 'Ukrainian', translatedName: 'Ukrainian',
emoji: '🇺🇦', emoji: '🇺🇦',
country: 'UA', country: 'UA',
momentLocale: 'uk', locale: 'uk',
}, },
// Vietnamese // Vietnamese
{ {
@@ -176,7 +176,7 @@ export const languages: Language[] = [
translatedName: 'Vietnamese', translatedName: 'Vietnamese',
emoji: '🇻🇳', emoji: '🇻🇳',
country: 'VN', country: 'VN',
momentLocale: 'vi', locale: 'vi',
}, },
{ {
shortName: 'zh', shortName: 'zh',
@@ -184,15 +184,15 @@ export const languages: Language[] = [
translatedName: 'Chinese', translatedName: 'Chinese',
emoji: '🇨🇳', emoji: '🇨🇳',
country: 'CN', country: 'CN',
momentLocale: 'cn' locale: 'zh-cn',
}, },
{ {
originalName: 'Ελληνικά', originalName: 'Ελληνικά',
translatedName: 'Greek', translatedName: 'Greek',
emoji: '🇬🇷', emoji: '🇬🇷',
country: 'GR', country: 'GR',
momentLocale: 'el', shortName: 'gr',
shortName: 'gr' locale: 'el',
}, },
{ {
shortName: 'tr', shortName: 'tr',
@@ -200,7 +200,7 @@ export const languages: Language[] = [
translatedName: 'Turkish', translatedName: 'Turkish',
emoji: '🇹🇷', emoji: '🇹🇷',
country: 'TR', country: 'TR',
momentLocale: 'tr', locale: 'tr',
}, },
{ {
shortName: 'lv', shortName: 'lv',
@@ -208,7 +208,7 @@ export const languages: Language[] = [
translatedName: 'Latvian', translatedName: 'Latvian',
emoji: '🇱🇻', emoji: '🇱🇻',
country: 'LV', country: 'LV',
momentLocale: 'lv', locale: 'lv',
}, },
{ {
shortName: 'hr', shortName: 'hr',
@@ -216,7 +216,15 @@ export const languages: Language[] = [
translatedName: 'Croatian', translatedName: 'Croatian',
emoji: '🇭🇷', emoji: '🇭🇷',
country: 'HR', country: 'HR',
momentLocale: 'hr', locale: 'hr',
},
// Hungarian
{
shortName: 'hu',
originalName: 'Magyar',
translatedName: 'Hungarian',
emoji: '🇭🇺',
locale: 'hu',
}, },
]; ];

View File

@@ -35,6 +35,7 @@ export class PlexClient {
const playerElement = this.findElement('Player', videoElement.elements); const playerElement = this.findElement('Player', videoElement.elements);
const mediaElement = this.findElement('Media', videoElement.elements); const mediaElement = this.findElement('Media', videoElement.elements);
const sessionElement = this.findElement('Session', videoElement.elements); const sessionElement = this.findElement('Session', videoElement.elements);
const transcodingElement = this.findElement('TranscodeSession', videoElement.elements);
if (!playerElement || !mediaElement) { if (!playerElement || !mediaElement) {
return undefined; return undefined;
@@ -43,7 +44,6 @@ export class PlexClient {
const { videoCodec, videoFrameRate, audioCodec, audioChannels, height, width, bitrate } = const { videoCodec, videoFrameRate, audioCodec, audioChannels, height, width, bitrate } =
mediaElement; mediaElement;
const transcodingElement = this.findElement('TranscodeSession', videoElement.elements);
return { return {
id: sessionElement?.id as string | undefined, id: sessionElement?.id as string | undefined,
@@ -51,7 +51,10 @@ export class PlexClient {
userProfilePicture: userElement?.thumb as string | undefined, userProfilePicture: userElement?.thumb as string | undefined,
sessionName: `${playerElement.product} (${playerElement.title})`, sessionName: `${playerElement.product} (${playerElement.title})`,
currentlyPlaying: { 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), type: this.getCurrentlyPlayingType(videoElement.attributes?.type as string),
metadata: { metadata: {
video: { video: {

View File

@@ -74,6 +74,7 @@ const definition = defineWidget({
}; };
}, },
itemComponent({ data, onChange, delete: deleteData }) { itemComponent({ data, onChange, delete: deleteData }) {
const { t } = useTranslation('modules/bookmark');
const form = useForm({ const form = useForm({
initialValues: data, initialValues: data,
validate: { validate: {
@@ -83,15 +84,15 @@ const definition = defineWidget({
return undefined; return undefined;
} }
return 'Length must be between 1 and 100'; return t('item.validation.length100');
}, },
href: (value) => { href: (value) => {
if (!z.string().min(1).max(200).safeParse(value).success) { 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) { if (!z.string().url().safeParse(value).success) {
return 'Not a valid link'; return t('item.validation.invalidLink');
} }
return undefined; return undefined;
@@ -101,7 +102,7 @@ const definition = defineWidget({
return undefined; return undefined;
} }
return 'Length must be between 1 and 100'; return t('item.validation.length400');
}, },
}, },
validateInputOnChange: true, validateInputOnChange: true,
@@ -122,13 +123,13 @@ const definition = defineWidget({
<TextInput <TextInput
icon={<IconTypography size="1rem" />} icon={<IconTypography size="1rem" />}
{...form.getInputProps('name')} {...form.getInputProps('name')}
label="Name" label={t('item.name')}
withAsterisk withAsterisk
/> />
<TextInput <TextInput
icon={<IconLink size="1rem" />} icon={<IconLink size="1rem" />}
{...form.getInputProps('href')} {...form.getInputProps('href')}
label="URL" label={t('item.url')}
withAsterisk withAsterisk
/> />
<IconSelector <IconSelector
@@ -140,17 +141,17 @@ const definition = defineWidget({
/> />
<Switch <Switch
{...form.getInputProps('openNewTab')} {...form.getInputProps('openNewTab')}
label="Open in new tab" label={t('item.newTab')}
checked={form.values.openNewTab} checked={form.values.openNewTab}
/> />
<Switch <Switch
{...form.getInputProps('hideHostname')} {...form.getInputProps('hideHostname')}
label="Hide Hostname" label={t('item.hideHostname')}
checked={form.values.hideHostname} checked={form.values.hideHostname}
/> />
<Switch <Switch
{...form.getInputProps('hideIcon')} {...form.getInputProps('hideIcon')}
label="Hide Icon" label={t('item.hideIcon')}
checked={form.values.hideIcon} checked={form.values.hideIcon}
/> />
<Button <Button
@@ -159,11 +160,11 @@ const definition = defineWidget({
variant="light" variant="light"
type="button" type="button"
> >
Delete {t('item.delete')}
</Button> </Button>
{!form.isValid() && ( {!form.isValid() && (
<Alert color="red" icon={<IconAlertTriangle size="1rem" />}> <Alert color="red" icon={<IconAlertTriangle size="1rem" />}>
Did not save, because there were validation errors. Please adust your inputs {t('item.validation.errorMsg')}
</Alert> </Alert>
)} )}
</Stack> </Stack>
@@ -174,18 +175,9 @@ const definition = defineWidget({
layout: { layout: {
type: 'select', type: 'select',
data: [ data: [
{ { value: 'autoGrid', },
label: 'Auto Grid', { value: 'horizontal', },
value: 'autoGrid', { value: 'vertical', },
},
{
label: 'Horizontal',
value: 'horizontal',
},
{
label: 'Vertical',
value: 'vertical',
},
], ],
defaultValue: 'autoGrid', defaultValue: 'autoGrid',
}, },
@@ -206,10 +198,10 @@ interface BookmarkWidgetTileProps {
} }
function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) { function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
const { t } = useTranslation('modules/bookmark');
const { classes } = useStyles(); const { classes } = useStyles();
const { enabled: isEditModeEnabled } = useEditModeStore(); const { enabled: isEditModeEnabled } = useEditModeStore();
const { fn, colors, colorScheme } = useMantineTheme(); const { fn, colors, colorScheme } = useMantineTheme();
const { t } = useTranslation('modules/bookmark');
if (widget.properties.items.length === 0) { if (widget.properties.items.length === 0) {
return ( return (

View File

@@ -2,8 +2,9 @@ import { useMantineTheme } from '@mantine/core';
import { Calendar } from '@mantine/dates'; import { Calendar } from '@mantine/dates';
import { IconCalendarTime } from '@tabler/icons-react'; import { IconCalendarTime } from '@tabler/icons-react';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { i18n } from 'next-i18next';
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/router';
import { getLanguageByCode } from '~/tools/language';
import { RouterOutputs, api } from '~/utils/api'; import { RouterOutputs, api } from '~/utils/api';
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore'; import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
@@ -30,20 +31,20 @@ const definition = defineWidget({
type: 'select', type: 'select',
defaultValue: 'inCinemas', defaultValue: 'inCinemas',
data: [ data: [
{ label: 'In Cinemas', value: 'inCinemas' }, { value: 'inCinemas' },
{ label: 'Physical', value: 'physicalRelease' }, { value: 'physicalRelease' },
{ label: 'Digital', value: 'digitalRelease' }, { value: 'digitalRelease' },
], ],
}, },
fontSize: { fontSize: {
type: 'select', type: 'select',
defaultValue: 'xs', defaultValue: 'xs',
data: [ data: [
{ label: 'Extra Small', value: 'xs' }, { value: 'xs' },
{ label: 'Small', value: 'sm' }, { value: 'sm' },
{ label: 'Medium', value: 'md' }, { value: 'md' },
{ label: 'Large', value: 'lg' }, { value: 'lg' },
{ label: 'Extra Large', value: 'xl' }, { value: 'xl' },
], ],
}, },
}, },
@@ -63,6 +64,7 @@ interface CalendarTileProps {
} }
function CalendarTile({ widget }: CalendarTileProps) { function CalendarTile({ widget }: CalendarTileProps) {
const { locale } = useRouter();
const { colorScheme, radius } = useMantineTheme(); const { colorScheme, radius } = useMantineTheme();
const { name: configName } = useConfigContext(); const { name: configName } = useConfigContext();
const [month, setMonth] = useState(new Date()); const [month, setMonth] = useState(new Date());
@@ -72,6 +74,9 @@ function CalendarTile({ widget }: CalendarTileProps) {
enabled: !!sessionData?.user, enabled: !!sessionData?.user,
}); });
const language = getLanguageByCode(locale ?? 'en');
require(`dayjs/locale/${language.locale}.js`);
const { data: medias } = api.calendar.medias.useQuery( const { data: medias } = api.calendar.medias.useQuery(
{ {
configName: configName!, configName: configName!,
@@ -93,7 +98,7 @@ function CalendarTile({ widget }: CalendarTileProps) {
onPreviousMonth={setMonth} onPreviousMonth={setMonth}
onNextMonth={setMonth} onNextMonth={setMonth}
size={widget.properties.fontSize} size={widget.properties.fontSize}
locale={i18n?.resolvedLanguage ?? 'en'} locale={language.locale}
firstDayOfWeek={getFirstDayOfWeek(firstDayOfWeek)} firstDayOfWeek={getFirstDayOfWeek(firstDayOfWeek)}
hideWeekdays={widget.properties.hideWeekDays} hideWeekdays={widget.properties.hideWeekDays}
style={{ position: 'relative' }} style={{ position: 'relative' }}

View File

@@ -1,16 +1,21 @@
import { Stack, Text, createStyles } from '@mantine/core'; import { Stack, Text, createStyles } from '@mantine/core';
import { useElementSize } from '@mantine/hooks'; import { useElementSize } from '@mantine/hooks';
import { IconClock } from '@tabler/icons-react'; import { IconClock } from '@tabler/icons-react';
import moment from 'moment-timezone';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { getLanguageByCode } from '~/tools/language'; import { getLanguageByCode } from '~/tools/language';
import { api } from '~/utils/api'; 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 { useSetSafeInterval } from '../../hooks/useSetSafeInterval';
import { defineWidget } from '../helper'; import { defineWidget } from '../helper';
import { IWidget } from '../widgets'; import { IWidget } from '../widgets';
dayjs.extend(utc);
dayjs.extend(timezones);
const definition = defineWidget({ const definition = defineWidget({
id: 'date', id: 'date',
icon: IconClock, icon: IconClock,
@@ -24,14 +29,14 @@ const definition = defineWidget({
defaultValue: 'dddd, MMMM D', defaultValue: 'dddd, MMMM D',
data: () => [ data: () => [
{ value: 'hide' }, { value: 'hide' },
{ value: 'dddd, MMMM D', label: moment().format('dddd, MMMM D') }, { value: 'dddd, MMMM D', label: dayjs().format('dddd, MMMM D') },
{ value: 'dddd, D MMMM', label: moment().format('dddd, D MMMM') }, { value: 'dddd, D MMMM', label: dayjs().format('dddd, D MMMM') },
{ value: 'MMM D', label: moment().format('MMM D') }, { value: 'MMM D', label: dayjs().format('MMM D') },
{ value: 'D MMM', label: moment().format('D MMM') }, { value: 'D MMM', label: dayjs().format('D MMM') },
{ value: 'DD/MM/YYYY', label: moment().format('DD/MM/YYYY') }, { value: 'DD/MM/YYYY', label: dayjs().format('DD/MM/YYYY') },
{ value: 'MM/DD/YYYY', label: moment().format('MM/DD/YYYY') }, { value: 'MM/DD/YYYY', label: dayjs().format('MM/DD/YYYY') },
{ value: 'DD/MM', label: moment().format('DD/MM') }, { value: 'DD/MM', label: dayjs().format('DD/MM') },
{ value: 'MM/DD', label: moment().format('MM/DD') }, { value: 'MM/DD', label: dayjs().format('MM/DD') },
], ],
}, },
enableTimezone: { enableTimezone: {
@@ -84,11 +89,11 @@ function DateTile({ widget }: DateTileProps) {
className={cx(classes.extras, 'dashboard-tile-clock-city')} className={cx(classes.extras, 'dashboard-tile-clock-city')}
> >
{widget.properties.timezoneLocation.name} {widget.properties.timezoneLocation.name}
{widget.properties.titleState === 'both' && moment(date).format(' (z)')} {widget.properties.titleState === 'both' && dayjs(date).format(' (z)')}
</Text> </Text>
)} )}
<Text className={cx(classes.clock, 'dashboard-tile-clock-hour')}> <Text className={cx(classes.clock, 'dashboard-tile-clock-hour')}>
{moment(date).format(formatString)} {dayjs(date).format(formatString)}
</Text> </Text>
{!widget.properties.dateFormat.includes('hide') && ( {!widget.properties.dateFormat.includes('hide') && (
<Text <Text
@@ -96,7 +101,7 @@ function DateTile({ widget }: DateTileProps) {
pt="0.2rem" pt="0.2rem"
className={cx(classes.extras, 'dashboard-tile-clock-date')} className={cx(classes.extras, 'dashboard-tile-clock-date')}
> >
{moment(date).format(widget.properties.dateFormat)} {dayjs(date).format(widget.properties.dateFormat)}
</Text> </Text>
)} )}
</Stack> </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 const timeoutRef = useRef<NodeJS.Timeout>(); // reference for initial timeout until first minute change
useEffect(() => { useEffect(() => {
const language = getLanguageByCode(locale ?? 'en'); const language = getLanguageByCode(locale ?? 'en');
moment.locale(language.momentLocale); dayjs.locale(language.locale);
setDate(getNewDate(timezone)); setDate(getNewDate(timezone));
timeoutRef.current = setTimeout( timeoutRef.current = setTimeout(
() => { () => {
@@ -150,9 +155,8 @@ const useDateState = (location?: { latitude: number; longitude: number }) => {
}, 1000 * 60); }, 1000 * 60);
//1 minute - current seconds and milliseconds count //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); return () => timeoutRef.current && clearTimeout(timeoutRef.current);
}, [timezone, locale]); }, [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 //Returns a local date if no inputs or returns date from input zone
const getNewDate = (timezone?: string) => { const getNewDate = (timezone?: string) => {
if (timezone) { if (timezone) {
return moment().tz(timezone); return dayjs().tz(timezone);
} }
return moment(); return dayjs();
}; };
export default definition; export default definition;

View File

@@ -121,7 +121,7 @@ function IFrameTile({ widget }: IFrameTileProps) {
title="widget iframe" title="widget iframe"
allow={allowedPermissions.join(' ')} allow={allowedPermissions.join(' ')}
> >
<Text>Your Browser does not support iframes. Please update your browser.</Text> <Text>{t('card.errors.browserSupport')}</Text>
</iframe> </iframe>
</Container> </Container>
); );

View File

@@ -59,12 +59,13 @@ const useMediaRequestDecisionMutation = () => {
utils.mediaRequest.all.invalidate(); utils.mediaRequest.all.invalidate();
}, },
}); });
const { t } = useTranslation('modules/media-requests-list');
return async (variables: MediaRequestDecisionVariables) => { return async (variables: MediaRequestDecisionVariables) => {
const action = variables.isApproved ? 'Approving' : 'Declining'; const action = variables.isApproved ? t('mutation.approving') : t('mutation.declining');
notifications.show({ notifications.show({
id: `decide-${variables.request.id}`, id: `decide-${variables.request.id}`,
color: 'yellow', color: 'yellow',
title: `${action} request...`, title: `${action} ${t('mutation.request')}`,
message: undefined, message: undefined,
loading: true, loading: true,
}); });
@@ -76,7 +77,7 @@ const useMediaRequestDecisionMutation = () => {
}, },
{ {
onSuccess(_data, variables) { 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({ notifications.update({
id: `decide-${variables.id}`, id: `decide-${variables.id}`,
color: 'teal', color: 'teal',
@@ -189,7 +190,7 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
notifications.show({ notifications.show({
id: `approve ${item.id}`, id: `approve ${item.id}`,
color: 'yellow', color: 'yellow',
title: 'Approving request...', title: t('tooltips.approving'),
message: undefined, message: undefined,
loading: true, loading: true,
}); });

View File

@@ -16,8 +16,8 @@ const definition = defineWidget({
type: 'select', type: 'select',
defaultValue: 'row' as 'row' | 'column', defaultValue: 'row' as 'row' | 'column',
data: [ data: [
{ label: 'Horizontal', value: 'row' }, { value: 'row' },
{ label: 'Vertical', value: 'column' }, { value: 'column' },
], ],
}, },
}, },

View File

@@ -2,27 +2,29 @@ import { Card, Divider, Flex, Grid, Group, Text } from '@mantine/core';
import { IconDeviceMobile, IconId } from '@tabler/icons-react'; import { IconDeviceMobile, IconId } from '@tabler/icons-react';
import { GenericSessionInfo } from '../../types/api/media-server/session-info'; import { GenericSessionInfo } from '../../types/api/media-server/session-info';
import { useTranslation } from 'react-i18next';
export const DetailCollapseable = ({ session }: { session: GenericSessionInfo }) => { export const DetailCollapseable = ({ session }: { session: GenericSessionInfo }) => {
let details: { title: string; metrics: { name: string; value: string | undefined }[] }[] = []; let details: { title: string; metrics: { name: string; value: string | undefined }[] }[] = [];
const { t } = useTranslation('modules/media-server-list');
if (session.currentlyPlaying) { if (session.currentlyPlaying) {
if (session.currentlyPlaying.metadata.video) { if (session.currentlyPlaying.metadata.video) {
details = [ details = [
...details, ...details,
{ {
title: 'Video', title: t('detail.video.'),
metrics: [ metrics: [
{ {
name: 'Resolution', name: t('detail.video.resolution'),
value: `${session.currentlyPlaying.metadata.video.width}x${session.currentlyPlaying.metadata.video.height}`, 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, value: session.currentlyPlaying.metadata.video.videoFrameRate,
}, },
{ {
name: 'Codec', name: t('detail.video.codec'),
value: session.currentlyPlaying.metadata.video.videoCodec, value: session.currentlyPlaying.metadata.video.videoCodec,
}, },
{ {
@@ -39,14 +41,14 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
details = [ details = [
...details, ...details,
{ {
title: 'Audio', title: t('detail.audio.audio'),
metrics: [ metrics: [
{ {
name: 'Audio channels', name: t('detail.audio.channels'),
value: `${session.currentlyPlaying.metadata.audio.audioChannels}`, value: `${session.currentlyPlaying.metadata.audio.audioChannels}`,
}, },
{ {
name: 'Audio codec', name: t('detail.audio.codec'),
value: session.currentlyPlaying.metadata.audio.audioCodec, value: session.currentlyPlaying.metadata.audio.audioCodec,
}, },
], ],
@@ -58,24 +60,24 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
details = [ details = [
...details, ...details,
{ {
title: 'Transcoding', title: t('detail.transcoding.transcoding'),
metrics: [ metrics: [
{ {
name: 'Resolution', name: t('detail.video.resolution'),
value: `${session.currentlyPlaying.metadata.transcoding.width}x${session.currentlyPlaying.metadata.transcoding.height}`, 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, value: session.currentlyPlaying.metadata.transcoding.context,
}, },
{ {
name: 'Hardware encoding requested', name: t('detail.transcoding.requested'),
value: session.currentlyPlaying.metadata.transcoding.transcodeHwRequested value: session.currentlyPlaying.metadata.transcoding.transcodeHwRequested
? 'yes' ? 'yes'
: 'no', : 'no',
}, },
{ {
name: 'Source codec', name: t('detail.transcoding.source'),
value: value:
session.currentlyPlaying.metadata.transcoding.sourceAudioCodec || session.currentlyPlaying.metadata.transcoding.sourceAudioCodec ||
session.currentlyPlaying.metadata.transcoding.sourceVideoCodec session.currentlyPlaying.metadata.transcoding.sourceVideoCodec
@@ -83,7 +85,7 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
: undefined, : undefined,
}, },
{ {
name: 'Target codec', name: t('detail.transcoding.target'),
value: `${session.currentlyPlaying.metadata.transcoding.videoCodec} ${session.currentlyPlaying.metadata.transcoding.audioCodec}`, 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"> <Flex justify="space-between" mb="xs">
<Group> <Group>
<IconId size={16} /> <IconId size={16} />
<Text>ID</Text> <Text>{t('detail.id')}</Text>
</Group> </Group>
<Text>{session.id}</Text> <Text>{session.id}</Text>
</Flex> </Flex>
<Flex justify="space-between" mb="md"> <Flex justify="space-between" mb="md">
<Group> <Group>
<IconDeviceMobile size={16} /> <IconDeviceMobile size={16} />
<Text>Device</Text> <Text>{t('detail.device')}</Text>
</Group> </Group>
<Text>{session.sessionName}</Text> <Text>{session.sessionName}</Text>
</Flex> </Flex>
{details.length > 0 && ( {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> <Grid>
{details.map((detail, index) => ( {details.map((detail, index) => (

View File

@@ -42,7 +42,6 @@ interface MediaServerWidgetProps {
function MediaServerTile({ widget }: MediaServerWidgetProps) { function MediaServerTile({ widget }: MediaServerWidgetProps) {
const { t } = useTranslation('modules/media-server'); const { t } = useTranslation('modules/media-server');
const { config } = useConfigContext(); const { config } = useConfigContext();
const isEditMode = useEditModeStore((x) => x.enabled);
const { data, isError, isFetching, isInitialLoading } = useGetMediaServers({ const { data, isError, isFetching, isInitialLoading } = useGetMediaServers({
enabled: config !== undefined, enabled: config !== undefined,
@@ -72,7 +71,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
<Loader /> <Loader />
<Stack align="center" spacing={0}> <Stack align="center" spacing={0}>
<Text>{t('descriptor.name')}</Text> <Text>{t('descriptor.name')}</Text>
<Text color="dimmed">Homarr is loading streams...</Text> <Text color="dimmed">{t('descriptor.loading')}</Text>
</Stack> </Stack>
</Stack> </Stack>
); );

View File

@@ -3,6 +3,7 @@ import {
Icon, Icon,
IconDeviceTv, IconDeviceTv,
IconHeadphones, IconHeadphones,
IconMovie,
IconQuestionMark, IconQuestionMark,
IconVideo, IconVideo,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
@@ -23,6 +24,8 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
return IconHeadphones; return IconHeadphones;
case 'tv': case 'tv':
return IconDeviceTv; return IconDeviceTv;
case 'movie':
return IconMovie;
case 'video': case 'video':
return IconVideo; return IconVideo;
default: default:

View File

@@ -42,6 +42,7 @@ const definition = defineWidget({
dangerousAllowSanitizedItemContent: { dangerousAllowSanitizedItemContent: {
type: 'switch', type: 'switch',
defaultValue: false, defaultValue: false,
info: true,
}, },
textLinesClamp: { textLinesClamp: {
type: 'slider', type: 'slider',

View File

@@ -13,7 +13,7 @@ import {
createStyles, createStyles,
useMantineTheme, useMantineTheme,
} from '@mantine/core'; } from '@mantine/core';
import { useDisclosure, useElementSize } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { import {
IconAffiliate, IconAffiliate,
IconDatabase, IconDatabase,
@@ -37,9 +37,8 @@ interface TorrentQueueItemProps {
width: number; 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 [popoverOpened, { open: openPopover, close: closePopover }] = useDisclosure(false);
const theme = useMantineTheme();
const { classes } = useStyles(); const { classes } = useStyles();
const { t } = useTranslation('modules/torrents-status'); const { t } = useTranslation('modules/torrents-status');

View File

@@ -25,7 +25,7 @@ import { AppIntegrationType } from '../../types/app';
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed'; import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
import { defineWidget } from '../helper'; import { defineWidget } from '../helper';
import { IWidget } from '../widgets'; import { IWidget } from '../widgets';
import { BitTorrrentQueueItem } from './TorrentQueueItem'; import { BitTorrentQueueItem } from './TorrentQueueItem';
dayjs.extend(duration); dayjs.extend(duration);
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@@ -108,7 +108,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
<Loader /> <Loader />
<Stack align="center" spacing={0}> <Stack align="center" spacing={0}>
<Text>{t('card.loading.title')}</Text> <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>
</Stack> </Stack>
); );
@@ -156,7 +156,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
</thead> </thead>
<tbody> <tbody>
{filteredTorrents.map((torrent, index) => ( {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 && ( {filteredTorrents.length !== torrents.length && (

View File

@@ -21,8 +21,8 @@ import duration from 'dayjs/plugin/duration';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { FunctionComponent, useState } from 'react'; import { FunctionComponent, useState } from 'react';
import { useGetUsenetDownloads } from '../dashDot/api';
import { humanFileSize } from '../../tools/humanFileSize'; import { humanFileSize } from '../../tools/humanFileSize';
import { useGetUsenetDownloads } from '../dashDot/api';
dayjs.extend(duration); dayjs.extend(duration);
@@ -91,7 +91,6 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
<Table highlightOnHover style={{ tableLayout: 'fixed' }} ref={ref}> <Table highlightOnHover style={{ tableLayout: 'fixed' }} ref={ref}>
<thead> <thead>
<tr> <tr>
<th style={{ width: 32 }} />
<th style={{ width: '75%' }}>{t('queue.header.name')}</th> <th style={{ width: '75%' }}>{t('queue.header.name')}</th>
{sizeBreakpoint < width ? ( {sizeBreakpoint < width ? (
<th style={{ width: 100 }}>{t('queue.header.size')}</th> <th style={{ width: 100 }}>{t('queue.header.size')}</th>
@@ -107,21 +106,6 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
<tbody> <tbody>
{data.items.map((nzb) => ( {data.items.map((nzb) => (
<tr key={nzb.id}> <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> <td>
<Tooltip position="top" label={nzb.name}> <Tooltip position="top" label={nzb.name}>
<Text <Text

View File

@@ -11,7 +11,6 @@ import {
IconSun, IconSun,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useElementSize } from '@mantine/hooks';
interface WeatherIconProps { interface WeatherIconProps {
code: number; code: number;
@@ -25,7 +24,6 @@ interface WeatherIconProps {
*/ */
export const WeatherIcon = ({ code, size=50 }: WeatherIconProps) => { export const WeatherIcon = ({ code, size=50 }: WeatherIconProps) => {
const { t } = useTranslation('modules/weather'); const { t } = useTranslation('modules/weather');
const { width, ref } = useElementSize();
const { icon: Icon, name } = const { icon: Icon, name } =
weatherDefinitions.find((wd) => wd.codes.includes(code)) ?? unknownWeather; weatherDefinitions.find((wd) => wd.codes.includes(code)) ?? unknownWeather;

View File

@@ -4,7 +4,6 @@ import {
IconArrowDownRight, IconArrowDownRight,
IconArrowUpRight, IconArrowUpRight,
IconCloudRain, IconCloudRain,
IconCurrentLocation,
IconMapPin, IconMapPin,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
@@ -12,6 +11,7 @@ import { api } from '~/utils/api';
import { defineWidget } from '../helper'; import { defineWidget } from '../helper';
import { IWidget } from '../widgets'; import { IWidget } from '../widgets';
import { WeatherIcon } from './WeatherIcon'; import { WeatherIcon } from './WeatherIcon';
import { useTranslation } from 'react-i18next';
const definition = defineWidget({ const definition = defineWidget({
id: 'weather', id: 'weather',
@@ -52,6 +52,7 @@ interface WeatherTileProps {
function WeatherTile({ widget }: WeatherTileProps) { function WeatherTile({ widget }: WeatherTileProps) {
const { data: weather, isLoading, isError } = api.weather.at.useQuery(widget.properties.location); const { data: weather, isLoading, isError } = api.weather.at.useQuery(widget.properties.location);
const { width, ref } = useElementSize(); const { width, ref } = useElementSize();
const { t } = useTranslation('modules/weather');
if (isLoading) { if (isLoading) {
return ( return (
@@ -77,7 +78,7 @@ function WeatherTile({ widget }: WeatherTileProps) {
if (isError) { if (isError) {
return ( return (
<Center> <Center>
<Text weight={500}>An error occured</Text> <Text weight={500}>{t('error')}</Text>
</Center> </Center>
); );
} }

1214
yarn.lock

File diff suppressed because it is too large Load Diff