* feat: add professional three-branch release workflow automation (#1129)

- Add version-bump workflow for semantic versioning across all files
- Add beta-release workflow for automated pre-release testing
- Add production-release workflow with manual approval gates
- Add hotfix-release workflow for emergency patches
- Create comprehensive CONTRIBUTING.md with workflow guide
- Create detailed RELEASE_PROCESS.md for maintainers
- Add PR template with release checklists
- Update CODEOWNERS to protect workflow files
- Update README with contribution links
- Remove /docs from .gitignore to allow documentation

This implements a dev  beta  main branching strategy with:
- Automated version management across 6 files
- Changelog generation from conventional commits
- GitHub Releases with build artifacts
- Environment-based approvals for production
- Back-merge support for hotfixes

* feat: new default quotes experience, improve added page

* Fix/beta workflow version check (#1131)

* fix(workflows): prevent beta release for non-beta versions

* fix(workflows): address copilot PR review feedback

- Support iterative beta versions (7.6.0-beta.1 -> 7.6.0-beta.2)
- Remove tag trigger from beta workflow to prevent premature releases
- Fix tag format in docs/summaries to include 'v' prefix
- Clarify deployment approval wording

* feat: replace mui with new style

* feat: improve time formatting in Clock component with padded digits

* fix: change Checkbox component from label to div for better semantics

* fix: change Switch component from label to div for better semantics

* feat: add smooth animation to reset functionality in Slider component

* feat: enhance accessibility and styling for form components including Checkbox, Dropdown, Radio, Slider, and Text

* feat: enhance WeatherOptions component with improved layout and auto location reset functionality

* feat: update Slider and Dropdown components with improved layout and z-index adjustments

* feat: add reset functionality to Dropdown component with toast notification

* feat: update Dropdown component styles for improved layout and structure

* feat: update languageSettings component with increased padding for better spacing

* feat: bump version to 7.6.0 across all manifests and documentation

---------

Signed-off-by: Alex Sparkes <alexsparkes@gmail.com>
Co-authored-by: David Ralph <me@davidcralph.co.uk>
This commit is contained in:
Alex Sparkes
2026-01-25 20:58:57 +00:00
committed by GitHub
parent ecfb3c6648
commit 4cf5269cdc
50 changed files with 2186 additions and 742 deletions

View File

@@ -10,7 +10,6 @@ import {
Section,
} from 'components/Layout/Settings';
import { Checkbox, Switch, Text } from 'components/Form/Settings';
import { TextareaAutosize } from '@mui/material';
import { Button } from 'components/Elements';
import { toast } from 'react-toastify';
@@ -192,15 +191,15 @@ const GreetingOptions = ({ currentSubSection, onSubSectionChange, sectionName })
<span className="subtitle">
{variables.getMessage(`${GREETING_SECTION}.event_name`)}
</span>
<TextareaAutosize
<input
type="text"
className="text-field-input"
value={event.name}
placeholder={variables.getMessage(`${GREETING_SECTION}.event_name`)}
onChange={(e) => {
const updatedEvent = { ...event, name: e.target.value };
updateEvent(index, updatedEvent);
}}
varient="outlined"
style={{ padding: '0' }}
/>
</div>
</div>

View File

@@ -1,9 +1,16 @@
import variables from 'config/variables';
import React, { memo, useState, useMemo } from 'react';
import { MdAutoFixHigh, MdOutlineArrowForward, MdOutlineOpenInNew, MdCheckCircle } from 'react-icons/md';
import {
MdAutoFixHigh,
MdOutlineArrowForward,
MdOutlineOpenInNew,
MdCheckCircle,
MdOutlineUploadFile,
MdClose,
} from 'react-icons/md';
import placeholderIcon from 'assets/icons/marketplace-placeholder.png';
import { Button } from 'components/Elements';
import { Button, Tooltip } from 'components/Elements';
import Dropdown from '../../../../components/Form/Settings/Dropdown/Dropdown';
function filterItems(item, filter, categoryFilter) {
@@ -28,7 +35,29 @@ function filterItems(item, filter, categoryFilter) {
return textMatch && item.type === categoryMap[categoryFilter];
}
function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInstalled }) {
function getInitials(name) {
if (!name) return '??';
const words = name.split(' ');
if (words.length === 1) {
return name.substring(0, 2).toUpperCase();
}
return words
.slice(0, 2)
.map((word) => word[0])
.join('')
.toUpperCase();
}
function getTypeTranslationKey(type) {
const typeMap = {
photos: 'photo_packs',
quotes: 'quote_packs',
settings: 'preset_settings',
};
return typeMap[type] || type;
}
function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInstalled, isAdded, onUninstall }) {
item._onCollection = onCollection;
// Convert hex color to RGB for gradient with opacity
@@ -73,28 +102,62 @@ function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInsta
};
};
const isSideloaded = item.sideload === true;
return (
<div
className="item"
onClick={() => toggleFunction(item)}
className={`item ${isSideloaded ? 'item-sideloaded' : ''}`}
onClick={isSideloaded ? undefined : () => toggleFunction(item)}
key={item.name}
style={getGradientStyle()}
>
{isInstalled && item.colour && (
{isAdded && onUninstall && (
<Tooltip
title={variables.getMessage('modals.main.marketplace.product.buttons.remove')}
style={{ position: 'absolute', top: '12px', right: '12px', zIndex: 3 }}
>
<button
className="item-uninstall-btn"
onClick={(e) => {
e.stopPropagation();
onUninstall(item.type, item.name);
}}
>
<MdClose />
</button>
</Tooltip>
)}
{isSideloaded && (
<Tooltip
title={variables.getMessage('modals.main.addons.sideload.title')}
style={{ position: 'absolute', top: '12px', right: '48px', zIndex: 2 }}
>
<div className="item-sideload-badge">
<MdOutlineUploadFile />
</div>
</Tooltip>
)}
{isInstalled && item.colour && !isSideloaded && (
<div className="item-installed-badge" style={getBadgeStyle()}>
<MdCheckCircle />
</div>
)}
<img
className="item-icon"
alt="icon"
draggable={false}
src={item.icon_url}
onError={(e) => {
e.target.onerror = null;
e.target.src = placeholderIcon;
}}
/>
{item.icon_url ? (
<img
className="item-icon"
alt="icon"
draggable={false}
src={item.icon_url}
onError={(e) => {
e.target.onerror = null;
e.target.src = placeholderIcon;
}}
/>
) : (
<div className="item-icon item-icon-text">
{getInitials(item.display_name || item.name)}
</div>
)}
<div className="card-details">
<span className="card-title">{item.display_name || item.name}</span>
{!isCurator ? (
@@ -106,17 +169,14 @@ function ItemCard({ item, toggleFunction, type, onCollection, isCurator, isInsta
)}
<div className="card-chips">
{type === 'all' && !onCollection ? (
{item.type && (
<span className="card-type">
{variables.getMessage('modals.main.marketplace.' + item.type)}
{variables.getMessage('modals.main.marketplace.' + getTypeTranslationKey(item.type))}
</span>
) : null}
{/* {item.in_collections && item.in_collections.length > 0 && !onCollection ? (
<span className="card-collection">
{item.in_collections[0]}
</span>
) : null} */}
)}
{item.in_collections && item.in_collections.length > 0 && !onCollection && (
<span className="card-collection">{item.in_collections[0]}</span>
)}
</div>
</div>
</div>
@@ -136,6 +196,8 @@ function Items({
showCreateYourOwn,
filterOptions = false,
onSortChange,
isAdded = false,
onUninstall,
}) {
const [selectedCategory, setSelectedCategory] = useState('all');
const [sortType, setSortType] = useState(localStorage.getItem('sortMarketplace') || 'a-z');
@@ -239,6 +301,8 @@ function Items({
type={type}
onCollection={onCollection}
isInstalled={installedNames.has(item.name)}
isAdded={isAdded}
onUninstall={onUninstall}
key={index}
/>
))}

View File

@@ -84,27 +84,24 @@ const Added = memo(() => {
const sortAddons = useCallback((value, sendEvent) => {
const installedItems = JSON.parse(localStorage.getItem('installed'));
switch (value) {
case 'newest':
installedItems.reverse();
break;
case 'oldest':
break;
case 'a-z':
installedItems.sort((a, b) => {
if (a.display_name < b.display_name) {
return -1;
}
if (a.display_name > b.display_name) {
return 1;
}
return 0;
const nameA = (a.display_name || a.name || '').toLowerCase();
const nameB = (b.display_name || b.name || '').toLowerCase();
return nameA.localeCompare(nameB);
});
break;
case 'z-a':
installedItems.sort();
installedItems.reverse();
case 'recently-updated':
installedItems.sort((a, b) => {
const dateA = a.updated_at ? new Date(a.updated_at) : new Date(0);
const dateB = b.updated_at ? new Date(b.updated_at) : new Date(0);
return dateB - dateA;
});
break;
default:
break;
@@ -154,6 +151,12 @@ const Added = memo(() => {
setInstalled([]);
}, [installed]);
const handleUninstall = useCallback((type, name) => {
uninstall(type, name);
toast(variables.getMessage('toasts.uninstalled'));
setInstalled(JSON.parse(localStorage.getItem('installed')));
}, []);
useEffect(() => {
sortAddons(localStorage.getItem('sortAddons'), false);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
@@ -243,9 +246,8 @@ const Added = memo(() => {
onChange={(value) => sortAddons(value)}
items={[
{ value: 'newest', text: variables.getMessage('modals.main.addons.sort.newest') },
{ value: 'oldest', text: variables.getMessage('modals.main.addons.sort.oldest') },
{ value: 'a-z', text: variables.getMessage('modals.main.addons.sort.a_z') },
{ value: 'z-a', text: variables.getMessage('modals.main.addons.sort.z_a') },
{ value: 'recently-updated', text: 'Recently Updated' },
]}
/>
<Items
@@ -254,6 +256,7 @@ const Added = memo(() => {
filter=""
toggleFunction={(input) => toggle('item', input)}
showCreateYourOwn={false}
onUninstall={handleUninstall}
/>
</>
);

View File

@@ -2,9 +2,9 @@ import variables from 'config/variables';
import { useState } from 'react';
import { MdCancel, MdAdd, MdOutlineTextsms } from 'react-icons/md';
import { toast } from 'react-toastify';
import { TextareaAutosize } from '@mui/material';
import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings';
import { Textarea } from 'components/Form/Settings';
import { Button } from 'components/Elements';
import EventBus from 'utils/eventbus';
@@ -82,14 +82,13 @@ const MessageOptions = () => {
<span className="subtitle">
{variables.getMessage(`${MESSAGE_SECTION}.title`)}
</span>
<TextareaAutosize
<Textarea
value={messages[index]}
placeholder={variables.getMessage(
'modals.main.settings.sections.message.content',
)}
onChange={(e) => message(e, true, index)}
varient="outlined"
style={{ padding: '0' }}
minRows={2}
/>
</div>
</div>

View File

@@ -8,9 +8,45 @@ import Preview from '../../helpers/preview/Preview';
import EventBus from 'utils/eventbus';
import { parseDeepLink, shouldAutoOpenModal, updateHash } from 'utils/deepLinking';
import { install } from 'utils/marketplace';
import Welcome from 'features/welcome/Welcome';
const DEFAULT_PACK_ID = '0c8a5bdebd13';
const isDefaultPackInstalled = () => {
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
return installed.some((item) => item.id === DEFAULT_PACK_ID);
};
const isDefaultPackUninstalled = () => {
const uninstalledPacks = JSON.parse(localStorage.getItem('uninstalledPacks') || '[]');
return uninstalledPacks.includes(DEFAULT_PACK_ID);
};
const tryInstallDefaultPack = async () => {
// Don't install if offline mode, already installed, or explicitly uninstalled
if (
localStorage.getItem('offlineMode') === 'true' ||
isDefaultPackInstalled() ||
isDefaultPackUninstalled()
) {
return false;
}
try {
const response = await fetch(
`${variables.constants.API_URL}/marketplace/item/${DEFAULT_PACK_ID}`,
);
const { data } = await response.json();
install(data.type, data, false, true);
return true;
} catch (e) {
console.error('Failed to install default pack:', e);
return false;
}
};
const Modals = () => {
const [mainModal, setMainModal] = useState(false);
const [updateModal, setUpdateModal] = useState(false);
@@ -60,6 +96,15 @@ const Modals = () => {
localStorage.setItem('showReminder', false);
}
// Try to install default pack if it wasn't installed during welcome (e.g., no internet)
if (localStorage.getItem('showWelcome') !== 'true') {
tryInstallDefaultPack().then((installed) => {
if (installed) {
EventBus.emit('refresh', 'quote');
}
});
}
// Listen for EventBus modal open requests
const handleModalOpen = (data) => {
if (data === 'openMainModal') {
@@ -76,9 +121,12 @@ const Modals = () => {
};
}, []);
const closeWelcome = () => {
const closeWelcome = async () => {
localStorage.setItem('showWelcome', false);
setWelcomeModal(false);
await tryInstallDefaultPack();
EventBus.emit('refresh', 'widgetsWelcomeDone');
EventBus.emit('refresh', 'widgets');
EventBus.emit('refresh', 'backgroundwelcome');

View File

@@ -2,7 +2,6 @@ import variables from 'config/variables';
import { useState, memo } from 'react';
import { Checkbox, Slider } from 'components/Form/Settings';
import { Button } from 'components/Elements';
import { TextField } from '@mui/material';
import { toast } from 'react-toastify';
import EventBus from 'utils/eventbus';
@@ -39,22 +38,26 @@ function ExperimentalOptions() {
element=".other"
/>
<p style={{ textAlign: 'left', width: '100%' }}>Send Event</p>
<TextField
label={'Type'}
value={eventType}
onChange={(e) => setEventType(e.target.value)}
spellCheck={false}
varient="outlined"
InputLabelProps={{ shrink: true }}
/>
<TextField
label={'Name'}
value={eventName}
onChange={(e) => setEventName(e.target.value)}
spellCheck={false}
varient="outlined"
InputLabelProps={{ shrink: true }}
/>
<div className="text-field">
<label className="text-field-label">Type</label>
<input
type="text"
className="text-field-input"
value={eventType || ''}
onChange={(e) => setEventType(e.target.value)}
spellCheck={false}
/>
</div>
<div className="text-field">
<label className="text-field-label">Name</label>
<input
type="text"
className="text-field-input"
value={eventName || ''}
onChange={(e) => setEventName(e.target.value)}
spellCheck={false}
/>
</div>
<Button
type="settings"
onClick={() => EventBus.emit(eventType, eventName)}

View File

@@ -1,10 +1,9 @@
import { useState, useMemo } from 'react';
import { useT, useTranslation } from 'contexts/TranslationContext';
import { MdOutlineOpenInNew, MdSearch, MdComputer } from 'react-icons/md';
import { TextField, InputAdornment } from '@mui/material';
import { MdOutlineOpenInNew, MdComputer } from 'react-icons/md';
import { Radio, Checkbox } from 'components/Form/Settings';
import { Radio, Checkbox, SearchInput } from 'components/Form/Settings';
import languages from '@/i18n/languages.json';
import translationPercentages from '@/i18n/translationPercentages.json';
@@ -123,35 +122,10 @@ const LanguageOptions = () => {
marginBottom: 16,
}}
>
<TextField
<SearchInput
placeholder={t('modals.main.settings.sections.language.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
variant="outlined"
size="small"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<MdSearch style={{ color: '#888' }} />
</InputAdornment>
),
}}
sx={{
width: '250px',
'& .MuiOutlinedInput-root': {
borderRadius: '24px',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
'& fieldset': {
border: 'none',
},
'&:hover fieldset': {
border: 'none',
},
'&.Mui-focused fieldset': {
border: 'none',
},
},
}}
/>
{currentLangOption && (
<div style={{ color: '#888', whiteSpace: 'nowrap' }}>

View File

@@ -4,9 +4,9 @@ import { useT } from 'contexts';
import { MdContentCopy, MdAssignment, MdPushPin, MdDownload } from 'react-icons/md';
import { useFloating, shift } from '@floating-ui/react-dom';
import TextareaAutosize from '@mui/material/TextareaAutosize';
import { toast } from 'react-toastify';
import { Tooltip } from 'components/Elements';
import { Textarea } from 'components/Form/Settings';
import { saveFile } from 'utils/saveFile';
import EventBus from 'utils/eventbus';
@@ -112,12 +112,11 @@ const Notes = ({ notesRef, floatRef, position, xPosition, yPosition }) => {
</button>
</Tooltip>
</div>
<TextareaAutosize
<Textarea
placeholder={t('widgets.navbar.notes.placeholder')}
value={notes}
onChange={handleSetNotes}
minRows={5}
maxLength={10000}
/>
</div>
</span>

View File

@@ -9,11 +9,10 @@ import {
MdPlaylistAdd,
MdOutlineDragIndicator,
MdPlaylistRemove,
MdCheck,
} from 'react-icons/md';
import TextareaAutosize from '@mui/material/TextareaAutosize';
import { Tooltip } from 'components/Elements';
import Checkbox from '@mui/material/Checkbox';
import { Textarea } from 'components/Form/Settings';
import { shift, useFloating } from '@floating-ui/react-dom';
import {
DndContext,
@@ -210,15 +209,18 @@ function Todo({ todoRef, floatRef, position, xPosition, yPosition }) {
<SortableItem key={index} id={index}>
{({ attributes, listeners }) => (
<div className={'todoRow' + (todoItem.done ? ' done' : '')}>
<Checkbox
checked={todoItem.done}
<div
className={'todo-checkbox' + (todoItem.done ? ' checked' : '')}
onClick={() => updateTodo('done', index)}
/>
<TextareaAutosize
>
{todoItem.done && <MdCheck />}
</div>
<Textarea
placeholder={t('widgets.navbar.notes.placeholder')}
value={todoItem.value}
onChange={(data) => updateTodo('set', index, data)}
readOnly={todoItem.done}
minRows={1}
/>
<Tooltip
title={t(

View File

@@ -83,7 +83,13 @@ export function useQuoteLoader(updateQuote) {
const getQuote = useCallback(async () => {
const offline = localStorage.getItem('offlineMode') === 'true';
const type = localStorage.getItem('quoteType') || 'api';
let type = localStorage.getItem('quoteType') || 'quote_pack';
// Migrate deprecated 'api' type to 'quote_pack'
if (type === 'api') {
type = 'quote_pack';
localStorage.setItem('quoteType', 'quote_pack');
}
// Check for favourite quote first
const favouriteQuote = localStorage.getItem('favouriteQuote');
@@ -128,7 +134,8 @@ export function useQuoteLoader(updateQuote) {
});
}
case 'quote_pack': {
case 'quote_pack':
default: {
if (offline) return doOffline();
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
@@ -138,56 +145,31 @@ export function useQuoteLoader(updateQuote) {
...quote,
fallbackauthorimg: item.icon_url,
packName: item.display_name || item.name,
noAuthorImg: item.noAuthorImg || quote.noAuthorImg,
})));
if (quotePack.length === 0) return doOffline();
const data = quotePack[Math.floor(Math.random() * quotePack.length)];
const hasAuthor = data.author && data.author.trim() !== '';
const displayAuthor = hasAuthor ? data.author : data.packName;
// Try to get author image from Wikipedia unless pack disables it
let authorimgdata = { authorimg: data.fallbackauthorimg, authorimglicense: null };
if (hasAuthor && !data.noAuthorImg) {
const wikiImg = await getAuthorImg(data.author);
if (wikiImg.authorimg) {
authorimgdata = wikiImg;
}
}
return updateQuote({
quote: `"${data.quote}"`,
author: hasAuthor ? data.author : data.packName,
author: displayAuthor,
authorlink: hasAuthor ? getAuthorLink(data.author) : null,
authorimg: data.fallbackauthorimg,
...authorimgdata,
});
}
case 'api': {
if (offline) return doOffline();
const fetchAPIQuote = async () => {
const response = await fetch(
`${variables.constants.API_URL}/quotes/random`
).then(res => res.json());
if (response.statusCode === 429) return null;
const authorimgdata = await getAuthorImg(response.author);
return {
quote: `"${response.quote.replace(/\s+$/g, '')}"`,
author: response.author,
authorlink: getAuthorLink(response.author),
...authorimgdata,
authorOccupation: response.author_occupation,
};
};
try {
const data = JSON.parse(localStorage.getItem('nextQuote')) || await fetchAPIQuote();
localStorage.setItem('nextQuote', null);
if (data) {
updateQuote(data);
localStorage.setItem('currentQuote', JSON.stringify(data));
localStorage.setItem('nextQuote', JSON.stringify(await fetchAPIQuote()));
} else {
doOffline();
}
} catch {
doOffline();
}
break;
}
}
}, [updateQuote, getAuthorLink, getAuthorImg, doOffline]);

View File

@@ -1,7 +1,6 @@
import variables from 'config/variables';
import React, { useState } from 'react';
import { MdCancel, MdAdd, MdSource, MdOutlineFormatQuote } from 'react-icons/md';
import TextareaAutosize from '@mui/material/TextareaAutosize';
import {
Header,
@@ -11,7 +10,7 @@ import {
Section,
PreferencesWrapper,
} from 'components/Layout/Settings';
import { Checkbox, Dropdown } from 'components/Form/Settings';
import { Checkbox, Dropdown, Textarea } from 'components/Form/Settings';
import { Button } from 'components/Elements';
const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) => {
@@ -23,7 +22,15 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
return data;
};
const [quoteType, setQuoteType] = useState(localStorage.getItem('quoteType') || 'api');
const [quoteType, setQuoteType] = useState(() => {
let type = localStorage.getItem('quoteType') || 'quote_pack';
// Migrate deprecated 'api' type to 'quote_pack'
if (type === 'api') {
type = 'quote_pack';
localStorage.setItem('quoteType', 'quote_pack');
}
return type;
});
const [customQuote, setCustomQuote] = useState(getCustom());
const handleCustomQuote = (e, text, index, type) => {
@@ -93,10 +100,6 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
value: 'quote_pack',
text: variables.getMessage('modals.main.marketplace.title'),
},
{
value: 'api',
text: variables.getMessage('modals.main.settings.sections.background.type.api'),
},
{ value: 'custom', text: variables.getMessage(`${QUOTE_SECTION}.custom`) },
]}
/>
@@ -162,23 +165,23 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
<MdOutlineFormatQuote />
</div>
<div className="messageText">
<TextareaAutosize
<Textarea
value={customQuote[index].quote}
placeholder={variables.getMessage(
'modals.main.settings.sections.quote.title',
)}
onChange={(e) => handleCustomQuote(e, true, index, 'quote')}
varient="outlined"
style={{ fontSize: '22px', fontWeight: 'bold' }}
minRows={1}
/>
<TextareaAutosize
<Textarea
value={customQuote[index].author}
placeholder={variables.getMessage(
'modals.main.settings.sections.quote.author',
)}
className="subtitle"
onChange={(e) => handleCustomQuote(e, true, index, 'author')}
varient="outlined"
minRows={1}
/>
</div>
<div>

View File

@@ -8,6 +8,13 @@ import EventBus from 'utils/eventbus';
import './clock.scss';
// Helper function to format padded time values while preserving padding
const formatPaddedDigits = (value) => {
const str = String(value);
// Format each digit individually to preserve padding with locale numerals
return str.split('').map(digit => formatDigits(digit)).join('');
};
const Clock = () => {
const [timeType] = useState(localStorage.getItem('timeType'));
const [time, setTime] = useState('');
@@ -51,23 +58,22 @@ const Clock = () => {
if (localStorage.getItem('seconds') === 'true') {
const secs = ('00' + now.getSeconds()).slice(-2);
sec = `:${formatDigits(secs)}`;
setFinalSeconds(formatDigits(secs));
sec = `:${formatPaddedDigits(secs)}`;
setFinalSeconds(formatPaddedDigits(secs));
}
if (localStorage.getItem('timeformat') === 'twentyfourhour') {
if (zero === 'false') {
const hours = now.getHours();
const minutes = ('00' + now.getMinutes()).slice(-2);
time = `${formatDigits(hours)}:${formatDigits(minutes)}${sec}`;
setFinalHour(formatDigits(hours));
setFinalMinute(formatDigits(minutes));
} else {
const minutes = ('00' + now.getMinutes()).slice(-2);
if (zero === 'true') {
const hours = ('00' + now.getHours()).slice(-2);
const minutes = ('00' + now.getMinutes()).slice(-2);
time = `${formatDigits(hours)}:${formatDigits(minutes)}${sec}`;
time = `${formatPaddedDigits(hours)}:${formatPaddedDigits(minutes)}${sec}`;
setFinalHour(formatPaddedDigits(hours));
setFinalMinute(formatPaddedDigits(minutes));
} else {
const hours = now.getHours();
time = `${formatDigits(hours)}:${formatPaddedDigits(minutes)}${sec}`;
setFinalHour(formatDigits(hours));
setFinalMinute(formatDigits(minutes));
setFinalMinute(formatPaddedDigits(minutes));
}
setTime(time);
@@ -82,17 +88,16 @@ const Clock = () => {
hours = 12;
}
if (zero === 'false') {
const minutes = ('00' + now.getMinutes()).slice(-2);
time = `${formatDigits(hours)}:${formatDigits(minutes)}${sec}`;
setFinalHour(formatDigits(hours));
setFinalMinute(formatDigits(minutes));
} else {
const minutes = ('00' + now.getMinutes()).slice(-2);
if (zero === 'true') {
const paddedHours = ('00' + hours).slice(-2);
const minutes = ('00' + now.getMinutes()).slice(-2);
time = `${formatDigits(paddedHours)}:${formatDigits(minutes)}${sec}`;
setFinalHour(formatDigits(paddedHours));
setFinalMinute(formatDigits(minutes));
time = `${formatPaddedDigits(paddedHours)}:${formatPaddedDigits(minutes)}${sec}`;
setFinalHour(formatPaddedDigits(paddedHours));
setFinalMinute(formatPaddedDigits(minutes));
} else {
time = `${formatDigits(hours)}:${formatPaddedDigits(minutes)}${sec}`;
setFinalHour(formatDigits(hours));
setFinalMinute(formatPaddedDigits(minutes));
}
setTime(time);

View File

@@ -3,7 +3,6 @@ import { MdAutoAwesome } from 'react-icons/md';
import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings';
import { useLocalStorageState } from 'utils/useLocalStorageState';
import { Radio, Dropdown, Checkbox } from 'components/Form/Settings';
import { TextField } from '@mui/material';
import variables from 'config/variables';
const useWeatherSettings = () => {
@@ -82,18 +81,26 @@ const WeatherOptions = () => {
<Row>
<Content title={variables.getMessage(`${WEATHER_SECTION}.location`)} />
<Action>
<TextField
label={variables.getMessage(`${WEATHER_SECTION}.location`)}
value={location}
onChange={changeLocation}
placeholder="London"
variant="outlined"
InputLabelProps={{ shrink: true }}
/>
<span className="link" onClick={getAutoLocation}>
<MdAutoAwesome />
{variables.getMessage(`${WEATHER_SECTION}.auto`)}
</span>
<div className="text-field-container">
<div className="text-field">
<div className="text-field-header">
<label className="text-field-label">
{variables.getMessage(`${WEATHER_SECTION}.location`)}
</label>
<span className="text-field-reset" onClick={getAutoLocation}>
<MdAutoAwesome />
{variables.getMessage(`${WEATHER_SECTION}.auto`)}
</span>
</div>
<input
type="text"
className="text-field-input"
value={location}
onChange={changeLocation}
placeholder="London"
/>
</div>
</div>
</Action>
</Row>
);

View File

@@ -1,12 +1,11 @@
import { useState, useMemo } from 'react';
import { MdOutlineOpenInNew, MdSearch } from 'react-icons/md';
import { TextField, InputAdornment } from '@mui/material';
import { MdOutlineOpenInNew } from 'react-icons/md';
import languages from '@/i18n/languages.json';
import translationPercentages from '@/i18n/translationPercentages.json';
import { useT, useTranslation } from 'contexts/TranslationContext';
import variables from 'config/variables';
import { Radio } from 'components/Form/Settings';
import { Radio, SearchInput } from 'components/Form/Settings';
import { Header, Content } from '../Layout';
function ChooseLanguage() {
@@ -107,37 +106,14 @@ function ChooseLanguage() {
{t('modals.main.settings.sections.language.use_system')} ({systemLanguage.name})
</button>
)}
<TextField
placeholder={t('modals.main.settings.sections.language.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
variant="outlined"
size="small"
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<MdSearch style={{ color: '#888' }} />
</InputAdornment>
),
}}
sx={{
marginBottom: 2,
'& .MuiOutlinedInput-root': {
borderRadius: '24px',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
'& fieldset': {
border: 'none',
},
'&:hover fieldset': {
border: 'none',
},
'&.Mui-focused fieldset': {
border: 'none',
},
},
}}
/>
<div style={{ marginBottom: 16 }}>
<SearchInput
placeholder={t('modals.main.settings.sections.language.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
fullWidth
/>
</div>
<div className="languageSettings">
<Radio name="language" options={filteredLanguages} category="welcomeLanguage" />
</div>