@@ -336,69 +338,6 @@ export default function MainLibrary(props: MainLibraryProps) {
as="div"
className="absolute bottom-8 left-8 lg:left-16 space-y-1 z-10 drop-shadow-sm"
>
-
- {t('library.splash.imagesBy')}{' '}
-
- Timon Käch
-
-
- {appVersion && (
-
-
- {
- if (isUpdateAvailable) {
- open('https://github.com/CyberTimon/RapidRAW/releases/latest');
- }
- }}
- data-tooltip={
- isUpdateAvailable
- ? t('library.splash.downloadVersion', { version: latestVersion })
- : t('library.splash.latestVersion')
- }
- >
-
- {t('library.splash.version', { version: appVersion })}
-
- {isUpdateAvailable && (
-
- {t('library.splash.newVersionAvailable')}
-
- )}
-
-
-
-
-
-
- {t('library.splash.donate')}
-
- {t('library.splash.or')}
-
- {t('library.splash.contribute')}
-
-
-
- )}
>
)}
diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx
index c4496e1a00..584ebe5e75 100644
--- a/src/components/panel/SettingsPanel.tsx
+++ b/src/components/panel/SettingsPanel.tsx
@@ -18,6 +18,10 @@ import {
Image as ImageIcon,
Mouse,
Touchpad,
+ Smartphone,
+ Zap,
+ Save,
+ Palette,
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
@@ -551,6 +555,7 @@ export default function SettingsPanel({
const [logPathLoading, setLogPathLoading] = useState(true);
const [logPathError, setLogPathError] = useState(false);
const [dpr, setDpr] = useState(() => (typeof window !== 'undefined' ? window.devicePixelRatio : 1));
+ const [colorScienceConfig, setColorScienceConfig] = useState
(null);
const settingCategories = useMemo(
() => [
@@ -611,6 +616,34 @@ export default function SettingsPanel({
[t],
);
+ const colorSciencePipelineOptions = useMemo[]>(
+ () => [
+ { value: 'SimplifiedAces', label: t('settings.colorScience.pipelineSimplifiedAces') },
+ { value: 'Aces20', label: t('settings.colorScience.pipelineAces20') },
+ { value: 'OpenDRT', label: t('settings.colorScience.pipelineOpenDRT') },
+ ],
+ [t],
+ );
+
+ const colorScienceDisplayColorSpaceOptions = useMemo[]>(
+ () => [
+ { value: 'SRGB', label: t('settings.colorScience.colorSpaceSrgb') },
+ { value: 'DisplayP3', label: t('settings.colorScience.colorSpaceP3') },
+ { value: 'Rec2020', label: t('settings.colorScience.colorSpaceRec2020') },
+ ],
+ [t],
+ );
+
+ const colorScienceEotfOptions = useMemo[]>(
+ () => [
+ { value: 'SRGB', label: t('settings.colorScience.eotfSrgb') },
+ { value: 'Gamma22', label: t('settings.colorScience.eotfGamma22') },
+ { value: 'Gamma24', label: t('settings.colorScience.eotfGamma24') },
+ { value: 'PQ', label: t('settings.colorScience.eotfPQ') },
+ ],
+ [t],
+ );
+
useEffect(() => {
if (typeof window === 'undefined') return;
@@ -674,6 +707,30 @@ export default function SettingsPanel({
invoke('get_lensfun_makers').then(setLensMakers).catch(console.error);
}, []);
+ useEffect(() => {
+ const initColorScienceConfig = async () => {
+ try {
+ const storedConfig = appSettings?.colorScienceConfig;
+ const configJson = storedConfig || JSON.stringify({
+ pipeline: 'SimplifiedAces',
+ display_color_space: 'SRGB',
+ eotf: 'SRGB',
+ peak_luminance: 203.0,
+ hdr_enabled: false,
+ });
+ const result = await invoke('color_science_get_config', { configJson });
+ const parsed = JSON.parse(result);
+ setColorScienceConfig(parsed);
+ if (!storedConfig) {
+ onSettingsChange({ ...appSettings, colorScienceConfig: result });
+ }
+ } catch (error) {
+ console.error('Failed to initialize color science config:', error);
+ }
+ };
+ initColorScienceConfig();
+ }, []);
+
const handleProcessingSettingChange = async (key: string, value: any) => {
setProcessingSettings((prev) => ({ ...prev, [key]: value }));
@@ -697,6 +754,21 @@ export default function SettingsPanel({
}
};
+ const handleColorScienceChange = async (updates: Record) => {
+ try {
+ const currentConfigJson = appSettings?.colorScienceConfig || JSON.stringify(colorScienceConfig);
+ const result = await invoke('color_science_update_config', {
+ configJson: currentConfigJson,
+ updates: JSON.stringify(updates),
+ });
+ const parsed = JSON.parse(result);
+ setColorScienceConfig(parsed);
+ onSettingsChange({ ...appSettings, colorScienceConfig: result });
+ } catch (error) {
+ console.error('Failed to update color science config:', error);
+ }
+ };
+
const handleSaveAndRelaunch = async () => {
await onSettingsChange({
...appSettings,
@@ -1325,6 +1397,139 @@ export default function SettingsPanel({
+ {osPlatform === 'android' && (
+
{t('settings.tagging.title')}
diff --git a/src/components/panel/right/CropPanel.tsx b/src/components/panel/right/CropPanel.tsx
index c770e49a74..15d94c7345 100644
--- a/src/components/panel/right/CropPanel.tsx
+++ b/src/components/panel/right/CropPanel.tsx
@@ -23,6 +23,7 @@ import Text from '../../ui/Text';
import Slider from '../../ui/Slider';
import { TEXT_COLOR_KEYS, TextColors, TextVariants, TextWeights } from '../../../types/typography';
import { useEditorStore } from '../../../store/useEditorStore';
+import { useUIStore } from '../../../store/useUIStore';
import { useEditorActions } from '../../../hooks/useEditorActions';
const BASE_RATIO = 1.618;
@@ -51,10 +52,11 @@ export default function CropPanel() {
const activeOverlay = useEditorStore((s) => s.overlayMode);
const setEditor = useEditorStore((s) => s.setEditor);
const { setAdjustments } = useEditorActions();
+ const isTransformModalOpen = useUIStore((s) => s.isTransformModalOpen);
+ const isLensCorrectionModalOpen = useUIStore((s) => s.isLensCorrectionModalOpen);
+ const setUI = useUIStore((s) => s.setUI);
const [customW, setCustomW] = useState('');
const [customH, setCustomH] = useState('');
- const [isTransformModalOpen, setIsTransformModalOpen] = useState(false);
- const [isLensModalOpen, setIsLensModalOpen] = useState(false);
const [isRotationActive, setIsRotationActive] = useState(false);
const [preferPortrait, setPreferPortrait] = useState(false);
const [isEditingCustom, setIsEditingCustom] = useState(false);
@@ -695,7 +697,7 @@ export default function CropPanel() {
setIsTransformModalOpen(true)}
+ onClick={() => setUI({ isTransformModalOpen: true })}
data-tooltip={t('editor.crop.tooltips.transform')}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
@@ -705,7 +707,7 @@ export default function CropPanel() {
setIsLensModalOpen(true)}
+ onClick={() => setUI({ isLensCorrectionModalOpen: true })}
data-tooltip={t('editor.crop.tooltips.lens')}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
@@ -730,7 +732,7 @@ export default function CropPanel() {
setIsTransformModalOpen(false)}
+ onClose={() => setUI({ isTransformModalOpen: false })}
onApply={(newParams) => {
setAdjustments((prev: Adjustments) => ({
...prev,
@@ -748,8 +750,8 @@ export default function CropPanel() {
/>
setIsLensModalOpen(false)}
+ isOpen={isLensCorrectionModalOpen}
+ onClose={() => setUI({ isLensCorrectionModalOpen: false })}
onApply={(newParams) => {
setAdjustments((prev: Adjustments) => ({
...prev,
diff --git a/src/components/panel/right/EditHistoryPanel.tsx b/src/components/panel/right/EditHistoryPanel.tsx
new file mode 100644
index 0000000000..abceeca58f
--- /dev/null
+++ b/src/components/panel/right/EditHistoryPanel.tsx
@@ -0,0 +1,297 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { invoke } from '@tauri-apps/api/core';
+import { Undo2, Redo2, GitBranch, Circle, SlidersHorizontal, Crop, Sparkles, Package } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import clsx from 'clsx';
+import Text from '../../ui/Text';
+import { TextColors, TextVariants, TextWeights } from '../../../types/typography';
+
+interface HistoryNode {
+ id: number;
+ label: string;
+ edit_type: string;
+ timestamp: number;
+ is_current: boolean;
+ is_branch_point: boolean;
+ branch_count: number;
+}
+
+interface EditHistoryPanelProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onHistorySelect?: (nodeId: number) => void;
+}
+
+function getEditTypeIcon(editType: string) {
+ switch (editType) {
+ case 'initial_state':
+ return Circle;
+ case 'adjustment':
+ return SlidersHorizontal;
+ case 'crop':
+ return Crop;
+ case 'mask_edit':
+ return Sparkles;
+ case 'batch_operation':
+ return Package;
+ default:
+ if (editType.startsWith('preset_')) return Sparkles;
+ return Circle;
+ }
+}
+
+export const EditHistoryPanel: React.FC = ({ isOpen, onClose, onHistorySelect }) => {
+ const { t } = useTranslation();
+ const [historyTree, setHistoryTree] = useState('');
+ const [historySummary, setHistorySummary] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (isOpen && !historyTree) {
+ initHistory();
+ }
+ }, [isOpen]);
+
+ const initHistory = async () => {
+ try {
+ setIsLoading(true);
+ const tree = await invoke('edit_history_new');
+ setHistoryTree(tree);
+ await refreshSummary(tree);
+ } catch (e) {
+ console.error('Failed to initialize edit history:', e);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const refreshSummary = async (tree: string) => {
+ try {
+ const summaryJson = await invoke('edit_history_get_summary', { treeJson: tree });
+ const summary = JSON.parse(summaryJson) as HistoryNode[];
+ setHistorySummary(summary);
+ } catch (e) {
+ console.error('Failed to get history summary:', e);
+ }
+ };
+
+ const getSummary = async (tree: string): Promise => {
+ const summaryJson = await invoke('edit_history_get_summary', { treeJson: tree });
+ return JSON.parse(summaryJson) as HistoryNode[];
+ };
+
+ const handleUndo = async () => {
+ try {
+ const newTree = await invoke('edit_history_undo', { treeJson: historyTree });
+ setHistoryTree(newTree);
+ await refreshSummary(newTree);
+ const summary = await getSummary(newTree);
+ const current = summary.find(n => n.is_current);
+ if (current && onHistorySelect) onHistorySelect(current.id);
+ } catch (e) {
+ console.error('Undo failed:', e);
+ }
+ };
+
+ const handleRedo = async () => {
+ try {
+ const newTree = await invoke('edit_history_redo', { treeJson: historyTree });
+ setHistoryTree(newTree);
+ await refreshSummary(newTree);
+ const summary = await getSummary(newTree);
+ const current = summary.find(n => n.is_current);
+ if (current && onHistorySelect) onHistorySelect(current.id);
+ } catch (e) {
+ console.error('Redo failed:', e);
+ }
+ };
+
+ const handleSwitchTo = async (nodeId: number) => {
+ try {
+ const newTree = await invoke('edit_history_switch_to', { treeJson: historyTree, nodeId });
+ setHistoryTree(newTree);
+ await refreshSummary(newTree);
+ if (onHistorySelect) onHistorySelect(nodeId);
+ } catch (e) {
+ console.error('Switch to history node failed:', e);
+ }
+ };
+
+ const handleCreateBranch = async (fromNodeId: number, branchName: string) => {
+ try {
+ const newTree = await invoke('edit_history_create_branch', {
+ treeJson: historyTree,
+ fromNodeId,
+ branchName,
+ });
+ setHistoryTree(newTree);
+ await refreshSummary(newTree);
+ } catch (e) {
+ console.error('Create branch failed:', e);
+ }
+ };
+
+ const formatTime = (timestamp: number): string => {
+ const date = new Date(timestamp * 1000);
+ return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
+ };
+
+ const canUndo = historySummary.some(n => n.is_current && n.id !== 1);
+ const currentNodeIndex = historySummary.findIndex(n => n.is_current);
+ const canRedo = currentNodeIndex >= 0 && currentNodeIndex < historySummary.length - 1;
+ const branchCount = historySummary.filter(n => n.is_branch_point).length;
+
+ if (!isOpen) return null;
+
+ return (
+
+ {/* Title bar */}
+
+
{t('editor.history.title')}
+
+
+
+
+
+
+ {/* History list */}
+
+ {isLoading ? (
+
+ ...
+
+ ) : historySummary.length === 0 ? (
+
+ {t('editor.history.noHistory')}
+
+ ) : (
+
+ {historySummary.map((node, index) => {
+ const Icon = getEditTypeIcon(node.edit_type);
+ const isLast = index === historySummary.length - 1;
+ return (
+
+ {/* Timeline connector line */}
+ {!isLast && (
+
+ )}
+
+ {/* Node row */}
+
handleSwitchTo(node.id)}
+ className={clsx(
+ 'flex items-center gap-3 px-2 py-1.5 rounded-lg cursor-pointer transition-colors',
+ node.is_current
+ ? 'bg-accent/10 border-l-2 border-accent'
+ : 'hover:bg-card-active border-l-2 border-transparent',
+ )}
+ >
+ {/* Icon circle */}
+
+ {node.is_branch_point ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Node info */}
+
+
+
+ {node.label}
+
+ {node.is_current && (
+
+ {t('editor.history.current')}
+
+ )}
+
+
+ {formatTime(node.timestamp)}
+ {node.branch_count > 1 && ` · ${node.branch_count} ${t('editor.history.branches', { count: node.branch_count })}`}
+
+
+
+ {/* Branch action */}
+ {node.is_branch_point && (
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Footer stats */}
+
+
+
+ {t('editor.history.steps', { count: historySummary.length })}
+
+
+ {t('editor.history.branches', { count: branchCount })}
+
+
+
+
+ );
+};
+
+export default EditHistoryPanel;
diff --git a/src/components/panel/right/ExportPanel.tsx b/src/components/panel/right/ExportPanel.tsx
index e72e98ebc9..6abb5fbc75 100644
--- a/src/components/panel/right/ExportPanel.tsx
+++ b/src/components/panel/right/ExportPanel.tsx
@@ -526,6 +526,7 @@ export default function ExportPanel({
outputFormat: selectedFormat.extensions[0],
currentEditPath: selectedImage?.path || null,
currentEditAdjustments: adjustments || null,
+ isAndroid: isAndroid,
});
}
} catch (error) {
diff --git a/src/components/panel/right/PresetsPanel.tsx b/src/components/panel/right/PresetsPanel.tsx
index 1c4613866e..661a6233a7 100644
--- a/src/components/panel/right/PresetsPanel.tsx
+++ b/src/components/panel/right/PresetsPanel.tsx
@@ -502,6 +502,8 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp
const [previews, setPreviews] = useState>({});
const [isGeneratingPreviews, setIsGeneratingPreviews] = useState(false);
const [configureModalState, setConfigureModalState] = useState({ isOpen: false, preset: null });
+ const isConfigurePresetModalOpen = useUIStore((s) => s.isConfigurePresetModalOpen);
+ const setUI = useUIStore((s) => s.setUI);
const [isAddFolderModalOpen, setIsAddFolderModalOpen] = useState(false);
const [renameFolderState, setRenameFolderState] = useState({ isOpen: false, folder: null });
const [expandedFolders, setExpandedFolders] = useState(new Set());
@@ -867,6 +869,7 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp
}
}
setConfigureModalState({ isOpen: false, preset: null });
+ setUI({ isConfigurePresetModalOpen: false });
};
const handleAddFolder = (name: string) => {
@@ -1071,7 +1074,7 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp
{
icon: Settings2,
label: t('editor.presets.menu.configurePreset'),
- onClick: () => setConfigureModalState({ isOpen: true, preset: data as Preset }),
+ onClick: () => { setConfigureModalState({ isOpen: true, preset: data as Preset }); setUI({ isConfigurePresetModalOpen: true }); },
},
{ type: OPTION_SEPARATOR },
{
@@ -1111,7 +1114,7 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp
{
icon: Plus,
label: t('editor.presets.menu.newPreset'),
- onClick: () => setConfigureModalState({ isOpen: true, preset: null }),
+ onClick: () => { setConfigureModalState({ isOpen: true, preset: null }); setUI({ isConfigurePresetModalOpen: true }); },
},
{
icon: FolderPlus,
@@ -1164,7 +1167,7 @@ export default function PresetsPanel({ onNavigateToCommunity }: PresetsPanelProp
setConfigureModalState({ isOpen: false, preset: null })}
+ onClose={() => { setConfigureModalState({ isOpen: false, preset: null }); setUI({ isConfigurePresetModalOpen: false }); }}
onSave={handleSaveConfiguredPreset}
/>
> = [
[{ id: Panel.Metadata, icon: Info, title: 'editor.switcher.tooltips.info' }],
[
{ id: Panel.Adjustments, icon: SlidersHorizontal, title: 'editor.switcher.tooltips.adjust' },
+ { id: Panel.History, icon: History, title: 'editor.switcher.tooltips.history' },
{ id: Panel.Crop, icon: Crop, title: 'editor.switcher.tooltips.crop' },
{ id: Panel.Masks, icon: Layers, title: 'editor.switcher.tooltips.masks' },
{ id: Panel.Ai, icon: Paintbrush, title: 'editor.switcher.tooltips.inpaint' },
diff --git a/src/components/ui/AppProperties.tsx b/src/components/ui/AppProperties.tsx
index 7cb10ba952..d43f14be3f 100644
--- a/src/components/ui/AppProperties.tsx
+++ b/src/components/ui/AppProperties.tsx
@@ -121,6 +121,7 @@ export enum Panel {
Ai = 'ai',
Crop = 'crop',
Export = 'export',
+ History = 'history',
Masks = 'masks',
Metadata = 'metadata',
Presets = 'presets',
@@ -149,6 +150,7 @@ export enum Theme {
Arctic = 'arctic',
Blue = 'blue',
Dark = 'dark',
+ DeepSpaceBlack = 'deep-space-black',
Grey = 'grey',
Light = 'light',
MutedGreen = 'muted-green',
@@ -210,6 +212,7 @@ export interface AppSettings {
exifOverlay?: ExifOverlay;
language?: string;
folderTreeSort?: FolderTreeSort;
+ colorScienceConfig?: string;
}
export interface BrushSettings {
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 2a01126357..a255f238f4 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -1,4 +1,5 @@
import clsx from 'clsx';
+import { hapticOnButtonPress } from '../../utils/hapticFeedback';
interface ButtonProps {
autoFocus?: boolean;
@@ -12,11 +13,17 @@ interface ButtonProps {
}
const Button = ({ children, onClick, disabled, className = '', ...props }: ButtonProps) => {
+ const handleClick = (e: React.MouseEvent) => {
+ hapticOnButtonPress();
+ onClick?.(e);
+ };
+
const baseClasses = `
- flex items-center justify-center gap-2
- font-semibold py-2 px-4 rounded-md
- text-button-text text-md
- transition-transform duration-200
+ flex items-center justify-center gap-2 sm:gap-3
+ font-semibold py-2.5 px-4 sm:px-5 rounded-md
+ text-button-text text-sm sm:text-base
+ min-h-[40px] sm:min-h-[44px]
+ transition-transform duration-200
hover:scale-[1.01] active:scale-[.98]
disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none disabled:hover:scale-100
`;
@@ -33,7 +40,7 @@ const Button = ({ children, onClick, disabled, className = '', ...props }: Butto
);
return (
-
-
+
diff --git a/src/components/views/EditorView.tsx b/src/components/views/EditorView.tsx
index b6c705936f..784dcc7e9e 100644
--- a/src/components/views/EditorView.tsx
+++ b/src/components/views/EditorView.tsx
@@ -14,6 +14,7 @@ import MasksPanel from '../panel/right/MasksPanel';
import AIPanel from '../panel/right/AIPanel';
import PresetsPanel from '../panel/right/PresetsPanel';
import ExportPanel from '../panel/right/ExportPanel';
+import EditHistoryPanel from '../panel/right/EditHistoryPanel';
import { useEditorStore } from '../../store/useEditorStore';
import { useUIStore } from '../../store/useUIStore';
@@ -237,6 +238,7 @@ export default function EditorView({
/>
)}
{renderedRightPanel === Panel.Ai &&
}
+ {renderedRightPanel === Panel.History &&
handleRightPanelSelect(Panel.Adjustments)} />}
)}
diff --git a/src/components/views/LibraryView.tsx b/src/components/views/LibraryView.tsx
index 65877c6b2f..1d0b38e7f0 100644
--- a/src/components/views/LibraryView.tsx
+++ b/src/components/views/LibraryView.tsx
@@ -1,6 +1,7 @@
import { useShallow } from 'zustand/react/shallow';
+import { lazy, Suspense } from 'react';
-import CommunityPage from '../panel/CommunityPage';
+const CommunityPage = lazy(() => import('../panel/CommunityPage'));
import MainLibrary from '../panel/MainLibrary';
import BottomBar from '../panel/BottomBar';
@@ -118,12 +119,14 @@ export default function LibraryView({
{activeView === 'community' ? (
-
setUI({ activeView: 'library' })}
- supportedTypes={supportedTypes}
- imageList={sortedImageList}
- currentFolderPath={currentFolderPath}
- />
+ }>
+
setUI({ activeView: 'library' })}
+ supportedTypes={supportedTypes}
+ imageList={sortedImageList}
+ currentFolderPath={currentFolderPath}
+ />
+
) : (
) {
if (selectedImage && !selectedImage.isReady && selectedImage.path) {
let isEffectActive = true;
- const loadMetadataEarly = async () => {
+ const loadMetadataEarly = async (): Promise => {
try {
useEditorStore.getState().patchesSentToBackend.clear();
await invoke('clear_session_caches').catch((e) => console.warn('Cache clear failed:', e));
const metadata: any = await invoke(Invokes.LoadMetadata, { path: selectedImage.path });
- if (!isEffectActive) return;
+ if (!isEffectActive) return false;
let initialAdjusts;
if (metadata.adjustments && !metadata.adjustments.is_null) {
@@ -46,8 +46,15 @@ export function useImageLoader(cachedEditStateRef: React.RefObject) {
setEditor({ adjustments: initialAdjusts });
resetHistory(initialAdjusts);
+ return true;
} catch (err) {
console.error('Failed to load metadata early:', err);
+ if (isEffectActive) {
+ toast.error(`Failed to load image metadata: ${err}`);
+ setEditor({ selectedImage: null });
+ setLibrary({ isViewLoading: false });
+ }
+ return false;
}
};
@@ -56,11 +63,20 @@ export function useImageLoader(cachedEditStateRef: React.RefObject) {
const loadImageResult: any = await invoke(Invokes.LoadImage, { path: selectedImage.path });
if (!isEffectActive) return;
+ if (!loadImageResult) {
+ throw new Error('Image loading returned no data');
+ }
+
const { width, height } = loadImageResult;
+ if (!width || !height || typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) {
+ throw new Error(`Invalid image dimensions: ${width}x${height}`);
+ }
+
setEditor({ originalSize: { width, height } });
- if (appSettings?.editorPreviewResolution) {
- const maxSize = appSettings.editorPreviewResolution;
+ const effectivePreviewResolution = appSettings?.editorPreviewResolution || (useSettingsStore.getState().osPlatform === 'android' ? 1280 : 0);
+ if (effectivePreviewResolution > 0) {
+ const maxSize = effectivePreviewResolution;
const aspectRatio = width / height;
if (width > height) {
@@ -73,7 +89,7 @@ export function useImageLoader(cachedEditStateRef: React.RefObject) {
setEditor({ previewSize: { width: pWidth, height: pHeight } });
}
} else {
- setEditor({ previewSize: { width: 0, height: 0 } });
+ setEditor({ previewSize: { width, height } });
}
setEditor((state) => {
@@ -116,8 +132,8 @@ export function useImageLoader(cachedEditStateRef: React.RefObject) {
};
const loadAll = async () => {
- await loadMetadataEarly();
- if (isEffectActive) {
+ const metadataSuccess = await loadMetadataEarly();
+ if (isEffectActive && metadataSuccess) {
await loadFullImageData();
}
};
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 06933c11e4..7760a39cfe 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -103,6 +103,7 @@
"dehaze": "Dehaze",
"luminance": "Luminance",
"noiseReduction": "Noise Reduction",
+ "portrait": "Portrait",
"presence": "Presence",
"redCyan": "Red/Cyan",
"sharpening": "Sharpening",
@@ -507,6 +508,21 @@
"exporting": "Exporting…",
"savesTo": "External edit — saves to"
},
+ "history": {
+ "title": "Edit History",
+ "undo": "Undo",
+ "redo": "Redo",
+ "steps": "{{count}} steps",
+ "branches": "{{count}} branch points",
+ "createBranch": "New Branch",
+ "branchName": "Branch name",
+ "initialState": "Initial State",
+ "adjustmentChange": "Adjustment",
+ "cropChange": "Crop",
+ "presetApplied": "Preset",
+ "noHistory": "No edits yet",
+ "current": "Current"
+ },
"masks": {
"actions": {
"addNewComponent": "Add New Component",
@@ -698,6 +714,7 @@
"adjust": "Adjust",
"crop": "Crop",
"export": "Export",
+ "history": "Edit History",
"info": "Info",
"inpaint": "Inpaint",
"masks": "Masks",
@@ -965,7 +982,7 @@
},
"splash": {
"addFolder": "Add Folder",
- "brand": "RapidRAW",
+ "brand": "LightCraft",
"continueSession": "Continue Session",
"contribute": "Contribute on GitHub",
"descriptionAndroid": "A blazingly fast, GPU-accelerated RAW image editor. Open the library to begin.",
@@ -1507,6 +1524,14 @@
"saved": "Saved Lenses",
"title": "My Lenses"
},
+ "android": {
+ "title": "Android Settings",
+ "permissionsNote": "Android may require storage permissions to access and save photos. If images fail to load, please grant the necessary permissions in your device settings.",
+ "performanceMode": "Performance Mode",
+ "performanceModeDesc": "Reduce processing quality for smoother editing on low-end devices. This lowers preview resolution and disables live previews.",
+ "autoSave": "Auto-Save",
+ "autoSaveDesc": "Automatically save adjustments after each edit. Prevents data loss on devices where the app may be killed by the system."
+ },
"processing": {
"ai": {
"cloud": {
@@ -1645,6 +1670,29 @@
"workerThreads": "Thumbnail Worker Threads",
"workerThreadsDesc": "Number of parallel threads used to generate thumbnails. Higher values speed up library loading but use more CPU & RAM."
},
+ "colorScience": {
+ "title": "Color Science",
+ "pipeline": "Color Pipeline",
+ "pipelineDesc": "Select the color rendering pipeline for image processing.",
+ "pipelineSimplifiedAces": "Simplified ACES (Default)",
+ "pipelineAces20": "ACES 2.0",
+ "pipelineOpenDRT": "OpenDRT",
+ "displayColorSpace": "Display Color Space",
+ "displayColorSpaceDesc": "Target color space for the output image.",
+ "colorSpaceSrgb": "sRGB / Rec.709",
+ "colorSpaceP3": "Display P3",
+ "colorSpaceRec2020": "Rec.2020",
+ "eotf": "Transfer Function (EOTF)",
+ "eotfDesc": "Electro-optical transfer function for display encoding.",
+ "eotfSrgb": "sRGB",
+ "eotfGamma22": "Gamma 2.2",
+ "eotfGamma24": "Gamma 2.4 (BT.1886)",
+ "eotfPQ": "PQ (ST.2084)",
+ "hdrOutput": "HDR Output",
+ "hdrOutputDesc": "Enable High Dynamic Range output for compatible displays.",
+ "peakLuminance": "Peak Luminance",
+ "peakLuminanceDesc": "Maximum display brightness in nits. Higher values enable HDR highlights."
+ },
"tagging": {
"addCustomPlaceholder": "Add custom AI tags (comma separated)...",
"addCustomTooltip": "Add AI tag",
@@ -1674,26 +1722,32 @@
"title": "Tagging"
},
"thanks": {
- "description": "A huge thank you to the following projects that were very important in the development of RapidRAW:",
+ "description": "A developer who loves photography, building professional tools for photography enthusiasts",
"list": {
- "darktable": "For some reference implementations that guided parts of this work.",
+ "darktable": "I am just \"带娃的小陈工\", a developer who loves photography.",
"depth": "For the powerful monocular depth estimation model that enables the AI depth masking capabilities.",
"lama": "For the powerful & simple image inpainting model, which enables content-aware fill and object removal.",
"lensfun": "For its invaluable open-source library and comprehensive database for automatic lens correction.",
"negpy": "For the inspiration behind the negative conversion logic.",
- "nind": "For providing AI models that power the AI noise reduction capabilities in RapidRAW.",
+ "nind": "For providing AI models that power the AI noise reduction capabilities.",
"rawler": "For the excellent Rust crate that provides the foundation for RAW file processing in this project.",
"sam2": "For providing the foundation model used for the AI subject detection capabilities.",
"u2net": "For providing the robust architecture used for the AI sky and foreground detection capabilities.",
- "you": "For using and supporting RapidRAW. Your interest keeps this project alive and evolving.",
+ "you": "For using and supporting this project. Your interest keeps it alive and evolving.",
"youLabel": "You"
},
"title": "Special Thanks"
},
"themes": {
"dark": "Dark",
+ "deepSpaceBlack": "Deep Space Black",
"grey": "Grey",
- "light": "Light"
+ "light": "Light",
+ "snow": "Snow",
+ "arctic": "Arctic",
+ "blue": "Blue",
+ "mutedGreen": "Muted Green",
+ "sepia": "Sepia"
},
"title": "Settings",
"tooltips": {
@@ -1782,5 +1836,20 @@
"vectorscope": "Vectorscope"
}
}
+ },
+ "privacy": {
+ "title": "Privacy Policy",
+ "intro": "Welcome to RapidRAW. Before using this application, please read and agree to our privacy policy. We are committed to protecting your personal information and processing your data in accordance with the law.",
+ "permissionsTitle": "Device Permissions",
+ "permissionsDesc": "RapidRAW requires storage permissions to read and edit your photos, network access to download AI models and presets, and optional EXIF location access to display photo metadata. All processing is performed locally on your device.",
+ "noCollectionTitle": "What We Do NOT Collect",
+ "noCollectionDesc": "We do not collect personal identity information, device location data, usage behavior data, or build user profiles. No third-party advertising or tracking SDKs are integrated.",
+ "aiTitle": "AI Features",
+ "aiDesc": "All AI-powered features (denoising, super-resolution, inpainting, segmentation, semantic search) run entirely on your device. Your images are never uploaded to any external server.",
+ "storageTitle": "Data Storage",
+ "storageDesc": "Your original images and editing data are stored locally on your device. Application caches are stored in private directories inaccessible to other apps. All network communication uses HTTPS/TLS encryption.",
+ "fullPolicy": "This is a summary of our privacy policy. Continuing means you agree to the full privacy policy available in the application settings.",
+ "agree": "Agree and Continue",
+ "decline": "Decline"
}
}
diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json
index 9be01bef13..0ff21aaa8f 100644
--- a/src/i18n/locales/zh-CN.json
+++ b/src/i18n/locales/zh-CN.json
@@ -103,6 +103,7 @@
"dehaze": "去雾",
"luminance": "亮度",
"noiseReduction": "降噪",
+ "portrait": "人像",
"presence": "质感",
"redCyan": "红/青",
"sharpening": "锐化",
@@ -288,9 +289,9 @@
},
"sections": {
"basic": "基础",
- "color": "颜色",
+ "color": "色彩",
"curves": "曲线",
- "details": "细节",
+ "details": "人像",
"effects": "效果"
},
"title": "调整",
@@ -507,6 +508,21 @@
"exporting": "正在导出…",
"savesTo": "外部编辑 — 保存至"
},
+ "history": {
+ "title": "编辑历史",
+ "undo": "撤销",
+ "redo": "重做",
+ "steps": "{{count}} 步操作",
+ "branches": "{{count}} 个分支点",
+ "createBranch": "新建分支",
+ "branchName": "分支名称",
+ "initialState": "初始状态",
+ "adjustmentChange": "调整",
+ "cropChange": "裁剪",
+ "presetApplied": "预设",
+ "noHistory": "暂无编辑记录",
+ "current": "当前"
+ },
"masks": {
"actions": {
"addNewComponent": "添加新组件",
@@ -698,6 +714,7 @@
"adjust": "调整",
"crop": "裁剪",
"export": "导出",
+ "history": "编辑历史",
"info": "信息",
"inpaint": "修复",
"masks": "蒙版",
@@ -965,7 +982,7 @@
},
"splash": {
"addFolder": "添加文件夹",
- "brand": "RapidRAW",
+ "brand": "光影工坊",
"continueSession": "继续会话",
"contribute": "在 GitHub 上贡献",
"descriptionAndroid": "一款极速、GPU 加速的 RAW 图像编辑器。打开图库以开始。",
@@ -1343,6 +1360,14 @@
"noiseReduction": "降噪",
"title": "调整可见性"
},
+ "android": {
+ "title": "Android 设置",
+ "permissionsNote": "Android 需要存储权限以访问和保存照片",
+ "performanceMode": "性能模式",
+ "performanceModeDesc": "降低处理质量以提升低端设备的响应速度",
+ "autoSave": "自动保存",
+ "autoSaveDesc": "每次编辑后自动保存,防止系统回收进程导致数据丢失"
+ },
"categories": {
"general": "通用",
"processing": "处理",
@@ -1645,6 +1670,29 @@
"workerThreads": "缩略图工作线程数",
"workerThreadsDesc": "用于生成缩略图的并行线程数。较高的值可加快图库加载速度,但会占用更多 CPU 和内存。"
},
+ "colorScience": {
+ "title": "色彩科学",
+ "pipeline": "色彩管线",
+ "pipelineDesc": "选择用于图像处理的色彩渲染管线。",
+ "pipelineSimplifiedAces": "简化 ACES(默认)",
+ "pipelineAces20": "ACES 2.0",
+ "pipelineOpenDRT": "OpenDRT",
+ "displayColorSpace": "显示色彩空间",
+ "displayColorSpaceDesc": "输出图像的目标色彩空间。",
+ "colorSpaceSrgb": "sRGB / Rec.709",
+ "colorSpaceP3": "Display P3",
+ "colorSpaceRec2020": "Rec.2020",
+ "eotf": "传输函数 (EOTF)",
+ "eotfDesc": "用于显示编码的电光传输函数。",
+ "eotfSrgb": "sRGB",
+ "eotfGamma22": "Gamma 2.2",
+ "eotfGamma24": "Gamma 2.4 (BT.1886)",
+ "eotfPQ": "PQ (ST.2084)",
+ "hdrOutput": "HDR 输出",
+ "hdrOutputDesc": "为兼容显示器启用高动态范围输出。",
+ "peakLuminance": "峰值亮度",
+ "peakLuminanceDesc": "显示器的最大亮度(尼特)。较高的值可呈现 HDR 高光。"
+ },
"tagging": {
"addCustomPlaceholder": "添加自定义 AI 标签(逗号分隔)...",
"addCustomTooltip": "添加 AI 标签",
@@ -1674,9 +1722,9 @@
"title": "标签"
},
"thanks": {
- "description": "衷心感谢以下项目,它们在 RapidRAW 的开发过程中非常重要:",
+ "description": "热爱摄影的开发者,为摄影爱好者打造专业工具",
"list": {
- "darktable": "为部分参考实现提供了指导。",
+ "darktable": "我只是「带娃的小陈工」,一名热爱摄影的开发者。",
"depth": "为强大的单目深度估计模型提供支持,实现了 AI 深度蒙版功能。",
"lama": "为强大且简单的图像修复模型提供支持,实现了内容感知填充和对象移除。",
"lensfun": "为无价的开源库和全面的数据库提供支持,实现了自动镜头校正。",
@@ -1685,15 +1733,21 @@
"rawler": "为优秀的 Rust 库提供支持,构成了本项目 RAW 文件处理的基础。",
"sam2": "为提供基础模型,用于 AI 主体检测功能。",
"u2net": "为提供稳健的架构,用于 AI 天空和前景检测功能。",
- "you": "感谢您使用和支持 RapidRAW。您的关注使本项目持续发展。",
+ "you": "感谢您使用和支持。您的关注使本项目持续发展。",
"youLabel": "您"
},
"title": "特别鸣谢"
},
"themes": {
"dark": "深色",
+ "deepSpaceBlack": "深空黑",
"grey": "灰色",
- "light": "浅色"
+ "light": "浅色",
+ "snow": "雪白",
+ "arctic": "极光蓝",
+ "blue": "深蓝",
+ "mutedGreen": "柔绿",
+ "sepia": "暖棕"
},
"title": "设置",
"tooltips": {
@@ -1782,5 +1836,20 @@
"vectorscope": "矢量示波器"
}
}
+ },
+ "privacy": {
+ "title": "隐私政策",
+ "intro": "欢迎使用 RapidRAW。在使用本应用之前,请阅读并同意我们的隐私政策。我们致力于保护您的个人信息,并依法处理您的数据。",
+ "permissionsTitle": "设备权限",
+ "permissionsDesc": "RapidRAW 需要存储权限以读取和编辑您的照片,需要网络访问权限以下载 AI 模型和预设,以及可选的 EXIF 位置信息访问权限以显示照片元数据。所有处理均在您的设备本地完成。",
+ "noCollectionTitle": "我们不会收集的信息",
+ "noCollectionDesc": "我们不会收集个人身份信息、设备位置数据、使用行为数据,也不会进行用户画像。未集成任何第三方广告或行为追踪 SDK。",
+ "aiTitle": "AI 功能",
+ "aiDesc": "所有 AI 功能(降噪、超分辨率、修复、分割、语义搜索)完全在您的设备上运行。您的图像不会被上传到任何外部服务器。",
+ "storageTitle": "数据存储",
+ "storageDesc": "您的原始图像和编辑数据保存在设备本地。应用缓存存储在私有目录中,其他应用无法访问。所有网络通信均使用 HTTPS/TLS 加密。",
+ "fullPolicy": "以上为隐私政策摘要。继续使用即表示您同意应用设置中提供的完整隐私政策。",
+ "agree": "同意并继续",
+ "decline": "拒绝"
}
}
diff --git a/src/store/useSettingsStore.ts b/src/store/useSettingsStore.ts
index a634c39c21..780fbbedcb 100644
--- a/src/store/useSettingsStore.ts
+++ b/src/store/useSettingsStore.ts
@@ -12,6 +12,7 @@ interface SettingsState {
// Actions
initPlatform: () => void;
+ isAndroid: () => boolean;
setAppSettings: (settings: AppSettings | null) => void;
setTheme: (theme: string) => void;
setSupportedTypes: (types: SupportedTypes | null) => void;
@@ -26,12 +27,15 @@ export const useSettingsStore = create((set, get) => ({
initPlatform: () => {
try {
- set({ osPlatform: platform() });
+ const p = platform();
+ set({ osPlatform: p });
} catch (_err) {
set({ osPlatform: '' });
}
},
+ isAndroid: () => get().osPlatform === 'android',
+
setAppSettings: (settings) => set({ appSettings: settings }),
setTheme: (theme) => set({ theme }),
diff --git a/src/store/useUIStore.ts b/src/store/useUIStore.ts
index f49eedfb6a..4fc0678ae8 100644
--- a/src/store/useUIStore.ts
+++ b/src/store/useUIStore.ts
@@ -114,6 +114,11 @@ interface UIState {
isRenameAlbumModalOpen: boolean;
albumActionTarget: string | null;
+ // Editor Modals
+ isConfigurePresetModalOpen: boolean;
+ isLensCorrectionModalOpen: boolean;
+ isTransformModalOpen: boolean;
+
// Complex Modal States
confirmModalState: ConfirmModalState;
panoramaModalState: PanoramaModalState;
@@ -164,6 +169,10 @@ export const useUIStore = create((set, get) => ({
isRenameAlbumModalOpen: false,
albumActionTarget: null,
+ isConfigurePresetModalOpen: false,
+ isLensCorrectionModalOpen: false,
+ isTransformModalOpen: false,
+
confirmModalState: { isOpen: false },
panoramaModalState: {
error: null,
diff --git a/src/styles.css b/src/styles.css
index 9068d4938a..e4042aa1d0 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -117,9 +117,14 @@
@apply text-text-primary font-sans;
}
+ /* 使用 :focus-visible 保留键盘焦点指示器,移除鼠标点击时的轮廓 */
*:focus {
outline: none !important;
}
+ *:focus-visible {
+ outline: 2px solid var(--app-accent) !important;
+ outline-offset: 2px;
+ }
.enable-color-transitions *,
.enable-color-transitions *::before,
@@ -439,3 +444,87 @@
text-shadow: 0 0 18px rgba(255, 255, 255, 0.3);
}
}
+
+/* ============================================================
+ 移动端适配:Android 触控优化
+ 针对国内摄影用户在小屏设备上的操作体验
+ ============================================================ */
+
+/* 触控设备:增大滑块拇指 */
+@media (pointer: coarse) {
+ .slider-input::-webkit-slider-thumb {
+ height: 24px;
+ width: 24px;
+ margin-top: -8px;
+ }
+ .slider-input::-moz-range-thumb {
+ height: 24px;
+ width: 24px;
+ }
+
+ /* 增大滚动条宽度 */
+ ::-webkit-scrollbar {
+ width: 14px;
+ height: 14px;
+ }
+}
+
+/* 移动端竖屏:小屏手机 (< 480px) */
+@media (pointer: coarse) and (max-width: 480px) {
+ /* 增大间距 */
+ .mb-2 {
+ margin-bottom: 0.75rem;
+ }
+
+ /* 按钮最小触控区域 */
+ button,
+ [role='button'],
+ input[type='range'] {
+ min-height: 44px;
+ }
+
+ /* 输入框增大 */
+ input[type='text'],
+ input[type='number'] {
+ min-height: 44px;
+ font-size: 16px !important;
+ }
+}
+
+/* 极窄屏幕 (< 360px) */
+@media (pointer: coarse) and (max-width: 360px) {
+ button {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8125rem;
+ }
+}
+
+/* Android / Mobile Touch Optimizations */
+@media (pointer: coarse) {
+ /* Ensure minimum touch target size */
+ button,
+ [role="button"],
+ .switch-container,
+ input[type="range"] {
+ min-height: 44px;
+ min-width: 44px;
+ }
+
+ /* Prevent text selection during touch interactions */
+ .no-select {
+ -webkit-user-select: none;
+ user-select: none;
+ }
+
+ /* Improve scroll performance */
+ * {
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ /* Optimize touch scrolling */
+ .overflow-y-auto,
+ .overflow-auto {
+ -webkit-overflow-scrolling: touch;
+ overscroll-behavior: contain;
+ }
+}
diff --git a/src/types/typography.ts b/src/types/typography.ts
index e622ff8a0c..742f7c3b47 100644
--- a/src/types/typography.ts
+++ b/src/types/typography.ts
@@ -47,53 +47,53 @@ export interface VariantConfig {
export const TextVariants: Record = {
displayLarge: {
- size: 'text-5xl',
+ size: 'text-3xl sm:text-4xl lg:text-5xl',
defaultWeight: 'bold',
defaultColor: 'primary',
defaultElement: 'h1',
extraClasses: 'text-shadow-shiny mb-4',
},
display: {
- size: 'text-3xl',
+ size: 'text-2xl sm:text-3xl',
defaultWeight: 'bold',
defaultColor: 'primary',
defaultElement: 'h1',
extraClasses: 'text-shadow-shiny',
},
headline: {
- size: 'text-2xl',
+ size: 'text-xl sm:text-2xl',
defaultWeight: 'bold',
defaultColor: 'primary',
defaultElement: 'h1',
extraClasses: 'text-shadow-shiny',
},
title: {
- size: 'text-xl',
+ size: 'text-lg sm:text-xl',
defaultWeight: 'bold',
defaultColor: 'primary',
defaultElement: 'h2',
extraClasses: 'text-shadow-shiny',
},
heading: {
- size: 'text-base',
+ size: 'text-sm sm:text-base',
defaultWeight: 'semibold',
defaultColor: 'primary',
defaultElement: 'h3',
},
body: {
- size: 'text-sm',
+ size: 'text-xs sm:text-sm',
defaultWeight: 'normal',
defaultColor: 'secondary',
defaultElement: 'p',
},
label: {
- size: 'text-sm',
+ size: 'text-xs sm:text-sm',
defaultWeight: 'medium',
defaultColor: 'secondary',
defaultElement: 'span',
},
small: {
- size: 'text-xs',
+ size: 'text-[10px] sm:text-xs',
defaultWeight: 'normal',
defaultColor: 'secondary',
defaultElement: 'p',
diff --git a/src/utils/hapticFeedback.ts b/src/utils/hapticFeedback.ts
new file mode 100644
index 0000000000..8a38e3a119
--- /dev/null
+++ b/src/utils/hapticFeedback.ts
@@ -0,0 +1,73 @@
+/**
+ * 触觉反馈工具 - 专为国内 Android 摄影用户优化
+ * 提供轻量级触觉反馈,提升交互体验
+ */
+
+const isAndroid = typeof navigator !== 'undefined' && /android/i.test(navigator.userAgent);
+
+// 振动持续时间 (ms) - 国内用户偏好短促清晰的反馈
+const HAPTIC_DURATION = {
+ light: 8, // 轻触 - 滑块微调
+ medium: 12, // 中等 - 按钮点击
+ heavy: 18, // 重触 - 确认操作
+ selection: 5, // 选择 - 切换选项
+ success: [10, 30, 10], // 成功 - 模式振动
+ error: [20, 40, 20], // 错误 - 模式振动
+} as const;
+
+/**
+ * 触发触觉反馈
+ * @param intensity - 反馈强度
+ */
+export function triggerHaptic(intensity: keyof typeof HAPTIC_DURATION = 'light'): void {
+ if (!isAndroid) return;
+
+ try {
+ const pattern = HAPTIC_DURATION[intensity];
+ if (Array.isArray(pattern)) {
+ navigator.vibrate(pattern);
+ } else {
+ navigator.vibrate(pattern);
+ }
+ } catch {
+ // 振动不可用时静默失败
+ }
+}
+
+/**
+ * 滑块交互时的触觉反馈
+ * 在值变化时提供微妙的触觉确认
+ */
+export function hapticOnSliderChange(): void {
+ triggerHaptic('light');
+}
+
+/**
+ * 按钮点击时的触觉反馈
+ */
+export function hapticOnButtonPress(): void {
+ triggerHaptic('medium');
+}
+
+/**
+ * 开关切换时的触觉反馈
+ */
+export function hapticOnToggle(): void {
+ triggerHaptic('selection');
+}
+
+/**
+ * 操作成功时的触觉反馈
+ */
+export function hapticOnSuccess(): void {
+ triggerHaptic('success');
+}
+
+/**
+ * 操作失败时的触觉反馈
+ */
+export function hapticOnError(): void {
+ triggerHaptic('error');
+}
+
+export default triggerHaptic;
\ No newline at end of file
diff --git a/src/utils/themes.ts b/src/utils/themes.ts
index 3b43634283..2148e1956e 100644
--- a/src/utils/themes.ts
+++ b/src/utils/themes.ts
@@ -8,6 +8,23 @@ export interface ThemeProps {
}
export const THEMES: Array = [
+ {
+ id: Theme.DeepSpaceBlack,
+ name: 'settings.themes.deepSpaceBlack',
+ splashImage: '/splash-dark.jpg',
+ cssVariables: {
+ '--app-bg-primary': 'rgb(6, 8, 12)',
+ '--app-bg-secondary': 'rgb(12, 15, 22)',
+ '--app-surface': 'rgb(9, 12, 18)',
+ '--app-card-active': 'rgb(18, 22, 32)',
+ '--app-button-text': 'rgb(6, 8, 12)',
+ '--app-text-primary': 'rgb(222, 228, 240)',
+ '--app-text-secondary': 'rgb(130, 142, 162)',
+ '--app-accent': 'rgb(25, 145, 125)',
+ '--app-border-color': 'rgb(22, 26, 36)',
+ '--app-hover-color': 'rgb(25, 145, 125)',
+ },
+ },
{
id: Theme.Dark,
name: 'settings.themes.dark',
@@ -36,10 +53,10 @@ export const THEMES: Array = [
'--app-card-active': 'rgb(250, 250, 250)',
'--app-button-text': 'rgb(255, 255, 255)',
'--app-text-primary': 'rgb(20, 20, 20)',
- '--app-text-secondary': 'rgb(108, 108, 108)',
- '--app-accent': 'rgb(198, 142, 110)',
- '--app-border-color': 'rgb(224, 224, 224)',
- '--app-hover-color': 'rgb(198, 142, 110)',
+ '--app-text-secondary': 'rgb(90, 90, 90)',
+ '--app-accent': 'rgb(178, 122, 90)',
+ '--app-border-color': 'rgb(210, 210, 210)',
+ '--app-hover-color': 'rgb(178, 122, 90)',
},
},
{
@@ -51,7 +68,7 @@ export const THEMES: Array = [
'--app-bg-secondary': 'rgb(118, 118, 118)',
'--app-surface': 'rgb(108, 108, 108)',
'--app-card-active': 'rgb(133, 133, 133)',
- '--app-button-text': 'rgb(45, 45, 45)',
+ '--app-button-text': 'rgb(255, 255, 255)',
'--app-text-primary': 'rgb(240, 240, 240)',
'--app-text-secondary': 'rgb(180, 180, 180)',
'--app-accent': 'rgb(220, 220, 220)',
@@ -59,6 +76,91 @@ export const THEMES: Array = [
'--app-hover-color': 'rgb(220, 220, 220)',
},
},
+ {
+ id: Theme.Snow,
+ name: 'settings.themes.snow',
+ splashImage: '/splash-light.jpg',
+ cssVariables: {
+ '--app-bg-primary': 'rgb(250, 250, 250)',
+ '--app-bg-secondary': 'rgb(255, 255, 255)',
+ '--app-surface': 'rgb(244, 244, 244)',
+ '--app-card-active': 'rgb(252, 252, 252)',
+ '--app-button-text': 'rgb(255, 255, 255)',
+ '--app-text-primary': 'rgb(30, 30, 30)',
+ '--app-text-secondary': 'rgb(100, 100, 100)',
+ '--app-accent': 'rgb(70, 130, 180)',
+ '--app-border-color': 'rgb(228, 228, 228)',
+ '--app-hover-color': 'rgb(70, 130, 180)',
+ },
+ },
+ {
+ id: Theme.Arctic,
+ name: 'settings.themes.arctic',
+ splashImage: '/splash-light.jpg',
+ cssVariables: {
+ '--app-bg-primary': 'rgb(240, 245, 250)',
+ '--app-bg-secondary': 'rgb(248, 251, 255)',
+ '--app-surface': 'rgb(235, 240, 248)',
+ '--app-card-active': 'rgb(245, 248, 252)',
+ '--app-button-text': 'rgb(255, 255, 255)',
+ '--app-text-primary': 'rgb(20, 30, 50)',
+ '--app-text-secondary': 'rgb(80, 100, 130)',
+ '--app-accent': 'rgb(60, 140, 200)',
+ '--app-border-color': 'rgb(210, 225, 240)',
+ '--app-hover-color': 'rgb(60, 140, 200)',
+ },
+ },
+ {
+ id: Theme.Blue,
+ name: 'settings.themes.blue',
+ splashImage: '/splash-dark.jpg',
+ cssVariables: {
+ '--app-bg-primary': 'rgb(18, 22, 36)',
+ '--app-bg-secondary': 'rgb(24, 30, 46)',
+ '--app-surface': 'rgb(22, 26, 40)',
+ '--app-card-active': 'rgb(32, 38, 56)',
+ '--app-button-text': 'rgb(255, 255, 255)',
+ '--app-text-primary': 'rgb(220, 230, 245)',
+ '--app-text-secondary': 'rgb(140, 155, 185)',
+ '--app-accent': 'rgb(80, 160, 240)',
+ '--app-border-color': 'rgb(35, 42, 60)',
+ '--app-hover-color': 'rgb(80, 160, 240)',
+ },
+ },
+ {
+ id: Theme.MutedGreen,
+ name: 'settings.themes.mutedGreen',
+ splashImage: '/splash-dark.jpg',
+ cssVariables: {
+ '--app-bg-primary': 'rgb(22, 28, 24)',
+ '--app-bg-secondary': 'rgb(28, 34, 30)',
+ '--app-surface': 'rgb(24, 30, 26)',
+ '--app-card-active': 'rgb(34, 42, 36)',
+ '--app-button-text': 'rgb(20, 28, 22)',
+ '--app-text-primary': 'rgb(215, 230, 220)',
+ '--app-text-secondary': 'rgb(140, 165, 150)',
+ '--app-accent': 'rgb(130, 200, 150)',
+ '--app-border-color': 'rgb(38, 48, 40)',
+ '--app-hover-color': 'rgb(130, 200, 150)',
+ },
+ },
+ {
+ id: Theme.Sepia,
+ name: 'settings.themes.sepia',
+ splashImage: '/splash-dark.jpg',
+ cssVariables: {
+ '--app-bg-primary': 'rgb(40, 35, 30)',
+ '--app-bg-secondary': 'rgb(48, 42, 36)',
+ '--app-surface': 'rgb(44, 38, 32)',
+ '--app-card-active': 'rgb(55, 48, 40)',
+ '--app-button-text': 'rgb(30, 25, 20)',
+ '--app-text-primary': 'rgb(235, 225, 210)',
+ '--app-text-secondary': 'rgb(175, 160, 140)',
+ '--app-accent': 'rgb(210, 170, 120)',
+ '--app-border-color': 'rgb(60, 50, 40)',
+ '--app-hover-color': 'rgb(210, 170, 120)',
+ },
+ },
];
-export const DEFAULT_THEME_ID = Theme.Dark;
+export const DEFAULT_THEME_ID = Theme.DeepSpaceBlack;