mirror of
https://github.com/mue/mue.git
synced 2026-07-16 05:23:49 +02:00
Dev (#1134)
* 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:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user