refactor: complete refactor for settings page

This commit is contained in:
Sam Chau
2024-09-18 13:07:45 +09:30
parent 98ed762e04
commit 7c6b476e96
10 changed files with 760 additions and 813 deletions

View File

@@ -38,12 +38,6 @@ const CommitSection = ({
/>
</>
)}
{hasUnstagedChanges && !hasStagedChanges && (
<p className='text-yellow-400 text-sm mb-2'>
You have unstaged changes. Stage your
changes before committing.
</p>
)}
</>
) : (
<div className='text-gray-300 text-sm italic'>

View File

@@ -1,98 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import Modal from "../ui/Modal";
const DiffModal = ({
isOpen,
onClose,
diffContent,
type,
name,
commitMessage,
title = "View Diff",
}) => {
const formatDiffContent = (content) => {
if (!content) return [];
return content.split("\n").map((line, index) => {
let lineClass = "py-1 pl-4 border-l-2 ";
if (line.startsWith("+")) {
lineClass += "bg-green-900/30 text-green-400 border-green-500";
} else if (line.startsWith("-")) {
lineClass += "bg-red-900/30 text-red-400 border-red-500";
} else {
lineClass += "border-transparent";
}
return (
<div key={index} className={`flex ${lineClass}`}>
<span className="w-12 text-gray-500 select-none text-right pr-4 border-r border-gray-700">
{index + 1}
</span>
<code className="flex-1 pl-4 font-mono text-sm">{line}</code>
</div>
);
});
};
const formattedContent = formatDiffContent(diffContent);
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} size="4xl">
<div className="space-y-4">
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg space-y-2 text-sm">
<div className="flex justify-between items-center">
<span className="font-medium text-gray-600 dark:text-gray-300">
Type:
</span>
<span className="bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded">
{type}
</span>
</div>
<div className="flex justify-between items-center">
<span className="font-medium text-gray-600 dark:text-gray-300">
Name:
</span>
<span className="bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 px-2 py-1 rounded">
{name === "Deleted File" ? "Deleted File" : name}
</span>
</div>
{commitMessage && (
<div className="flex flex-col">
<span className="font-medium text-gray-600 dark:text-gray-300 mb-1">
Commit Message:
</span>
<p className="bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 p-2 rounded">
{commitMessage}
</p>
</div>
)}
</div>
<div className="border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
<div className="bg-gray-50 dark:bg-gray-800 p-2 text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600">
Diff Content
</div>
<div className="bg-white dark:bg-gray-900 p-4 max-h-[60vh] overflow-y-auto">
{formattedContent.length > 0 ? (
formattedContent
) : (
<div className="text-gray-500 dark:text-gray-400 italic">
No differences found or file is empty.
</div>
)}
</div>
</div>
</div>
</Modal>
);
};
DiffModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
diffContent: PropTypes.string,
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
commitMessage: PropTypes.string,
title: PropTypes.string,
};
export default DiffModal;

View File

@@ -1,4 +1,4 @@
import React, {useState, useEffect} from 'react';
import React, { useState, useEffect } from 'react';
import {
getSettings,
getGitStatus,
@@ -6,58 +6,27 @@ import {
pushFiles,
revertFile,
pullBranch,
getDiff,
unlinkRepo,
checkDevMode
} from '../../api/api';
import SettingsBranchModal from './SettingsBranchModal';
import {
FileText,
Code,
AlertCircle,
Plus,
MinusCircle,
Edit,
GitBranch,
Loader,
Eye,
RotateCcw,
Download,
ArrowDown,
ArrowUp,
CheckCircle,
File,
Settings,
Unlink
} from 'lucide-react';
import SettingsBranchModal from './SettingsBranchModal';
import Alert from '../ui/Alert';
import CommitSection from './CommitSection';
import Tooltip from '../ui/Tooltip';
import DiffModal from './DiffModal';
import ArrContainer from './arrs/ArrContainer';
import RepoContainer from './git/RepoContainer';
import StatusContainer from './git/StatusContainer';
import { statusLoadingMessages, noChangesMessages, getRandomMessage } from '../../utils/messages';
const SettingsPage = () => {
const [settings, setSettings] = useState(null);
const [status, setStatus] = useState(null);
const [changes, setChanges] = useState(null);
const [isDevMode, setIsDevMode] = useState(false);
const [showBranchModal, setShowBranchModal] = useState(false);
const [loadingAction, setLoadingAction] = useState('');
const [loadingStatus, setLoadingStatus] = useState(true);
const [loadingMessage, setLoadingMessage] = useState('');
const [commitMessage, setCommitMessage] = useState('');
const [selectedIncomingChanges, setSelectedIncomingChanges] = useState([]);
const [selectedOutgoingChanges, setSelectedOutgoingChanges] = useState([]);
const [showDiffModal, setShowDiffModal] = useState(false);
const [diffContent, setDiffContent] = useState('');
const [currentChange, setCurrentChange] = useState(null);
const [loadingDiff, setLoadingDiff] = useState(false);
const [selectionType, setSelectionType] = useState(null);
const [funMessage, setFunMessage] = useState('');
const [sortConfig, setSortConfig] = useState({
key: 'type',
direction: 'descending'
});
const [statusLoading, setStatusLoading] = useState(true);
const [statusLoadingMessage, setStatusLoadingMessage] = useState('');
const [noChangesMessage, setNoChangesMessage] = useState('');
useEffect(() => {
fetchSettings();
@@ -86,72 +55,19 @@ const SettingsPage = () => {
}
};
const sortedChanges = changes => {
if (!sortConfig.key) return changes;
return [...changes].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? 1 : -1;
}
return 0;
});
};
const requestSort = key => {
let direction = 'ascending';
if (sortConfig.key === key && sortConfig.direction === 'ascending') {
direction = 'descending';
}
setSortConfig({key, direction});
};
const SortableHeader = ({children, sortKey}) => {
const isSorted = sortConfig.key === sortKey;
return (
<th
className='px-4 py-2 text-left text-gray-300 cursor-pointer hover:bg-gray-500'
onClick={() => requestSort(sortKey)}>
<div className='flex items-center'>
{children}
{isSorted &&
(sortConfig.direction === 'ascending' ? (
<ArrowUp size={14} className='ml-1' />
) : (
<ArrowDown size={14} className='ml-1' />
))}
</div>
</th>
);
};
const fetchGitStatus = async () => {
setLoadingStatus(true);
setLoadingMessage(getRandomLoadingMessage());
setFunMessage(getRandomFunMessage());
setStatusLoading(true);
setStatusLoadingMessage(getRandomMessage(statusLoadingMessages));
setNoChangesMessage(getRandomMessage(noChangesMessages));
try {
const result = await getGitStatus();
console.log(
'================ Git Status Response ================'
);
console.log(JSON.stringify(result, null, 2));
console.log(
'======================================================'
);
if (result.success) {
setStatus({
setChanges({
...result.data,
outgoing_changes: Array.isArray(
result.data.outgoing_changes
)
outgoing_changes: Array.isArray(result.data.outgoing_changes)
? result.data.outgoing_changes
: [],
incoming_changes: Array.isArray(
result.data.incoming_changes
)
incoming_changes: Array.isArray(result.data.incoming_changes)
? result.data.incoming_changes
: []
});
@@ -160,206 +76,16 @@ const SettingsPage = () => {
console.error('Error fetching Git status:', error);
Alert.error('Failed to fetch Git status');
} finally {
setLoadingStatus(false);
setStatusLoading(false);
}
};
const getRandomFunMessage = () => {
const funMessages = [
'No changes detected. Your regex is so precise, it could find a needle in a haystack... made of needles. 🧵🔍',
'All quiet on the commit front. Your custom formats are so perfect, even perfectionists are jealous. 🏆',
"No updates needed. Your media automation is running so smoothly, it's making butter jealous. 🧈",
'Zero modifications. Your torrent setup is seeding so efficiently, farmers are asking for advice. 🌾',
"No edits required. Your regex fu is so strong, it's bench-pressing parentheses for fun. 💪()",
'Unchanged status. Your Plex library is so well-organized, librarians are taking notes. 📚🤓',
"No alterations found. Your file naming scheme is so consistent, it's bringing tears to OCD eyes. 😢👀",
"All systems nominal. Your download queue is so orderly, it's making Marie Kondo question her career. 🧹✨",
"No revisions necessary. Your automation scripts are so smart, they're solving captchas for fun. 🤖🧩",
'Steady as she goes. Your media collection is so complete, Netflix is asking you for recommendations. 🎬👑'
];
return funMessages[Math.floor(Math.random() * funMessages.length)];
};
const renderChangeTable = (changes, title, icon, isIncoming) => (
<div className='mb-4'>
<h4 className='text-sm font-medium text-gray-200 mb-2 flex items-center'>
{icon}
<span>
{isIncoming
? title
: isDevMode
? 'Outgoing Changes'
: 'Local Changes'}{' '}
({changes.length})
</span>
</h4>
<div className='border border-gray-600 rounded-md overflow-hidden'>
<table className='w-full text-sm'>
<thead className='bg-gray-600'>
<tr>
<SortableHeader sortKey='status'>
Status
</SortableHeader>
<SortableHeader sortKey='type'>Type</SortableHeader>
<SortableHeader sortKey='name'>Name</SortableHeader>
<th className='px-4 py-2 text-left text-gray-300 w-1/5'>
Actions
</th>
<th className='px-4 py-2 text-right text-gray-300 w-1/10'>
Select
</th>
</tr>
</thead>
<tbody>
{sortedChanges(changes).map((change, index) => (
<tr
key={`${isIncoming ? 'incoming' : 'outgoing'}-${index}`}
className={`border-t border-gray-600 cursor-pointer hover:bg-gray-700 ${
(isIncoming
? selectedIncomingChanges
: selectedOutgoingChanges
).includes(change.file_path)
? 'bg-gray-700'
: ''
}`}
onClick={() =>
handleSelectChange(
change.file_path,
isIncoming
)
}>
<td className='px-4 py-2 text-gray-300'>
<div className='flex items-center'>
{getStatusIcon(change.status)}
<span className='ml-2'>
{change.staged
? `${change.status} (Staged)`
: change.status}
</span>
</div>
</td>
<td className='px-4 py-2 text-gray-300'>
<div className='flex items-center'>
{getTypeIcon(change.type)}
<span className='ml-2'>
{change.type}
</span>
</div>
</td>
<td className='px-4 py-2 text-gray-300'>
{change.name || 'Unnamed'}
</td>
<td className='px-4 py-2 text-left align-middle'>
<Tooltip content='View differences'>
<button
onClick={e => {
e.stopPropagation();
handleViewDiff(change);
}}
className='flex items-center justify-center px-2 py-1 bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors text-xs'
style={{width: '100%'}}>
{loadingDiff ? (
<Loader
size={12}
className='animate-spin'
/>
) : (
<>
<Eye
size={12}
className='mr-1'
/>
View Diff
</>
)}
</button>
</Tooltip>
</td>
<td className='px-4 py-2 text-right text-gray-300 align-middle'>
<input
type='checkbox'
checked={(isIncoming
? selectedIncomingChanges
: selectedOutgoingChanges
).includes(change.file_path)}
onChange={e => e.stopPropagation()}
disabled={!isIncoming && change.staged}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
const getStageButtonTooltip = () => {
if (selectionType === 'staged') {
return 'These files are already staged';
}
if (selectedOutgoingChanges.length === 0) {
return 'Select files to stage';
}
return 'Stage selected files';
};
const getCommitButtonTooltip = () => {
if (selectionType === 'unstaged') {
return 'You can only commit staged files';
}
if (selectedOutgoingChanges.length === 0) {
return 'Select files to commit';
}
if (!commitMessage.trim()) {
return 'Enter a commit message';
}
return 'Commit selected files';
};
const getRevertButtonTooltip = () => {
if (selectedOutgoingChanges.length === 0) {
return 'Select files to revert';
}
return 'Revert selected files';
};
const handleViewDiff = async change => {
setLoadingDiff(true);
try {
const response = await getDiff(change.file_path);
console.log('Diff response:', response); // Add this line to log the response
if (response.success) {
console.log('Diff content:', response.diff); // Add this line to log the diff content
setDiffContent(response.diff);
setCurrentChange(change);
setShowDiffModal(true);
} else {
Alert.error(response.error);
}
} catch (error) {
Alert.error(
'An unexpected error occurred while fetching the diff.'
);
console.error('Error fetching diff:', error);
} finally {
setLoadingDiff(false);
setLoadingAction('');
}
};
const handleStageSelectedChanges = async () => {
if (selectedOutgoingChanges.length === 0) {
Alert.warning('Please select at least one change to stage.');
return;
}
const handleStageSelectedChanges = async (selectedChanges) => {
setLoadingAction('stage_selected');
try {
const response = await addFiles(selectedOutgoingChanges);
const response = await addFiles(selectedChanges);
if (response.success) {
await fetchGitStatus();
setSelectedOutgoingChanges([]); // Clear the selected changes after staging
Alert.success(response.message);
} else {
Alert.error(response.error);
@@ -372,90 +98,49 @@ const SettingsPage = () => {
}
};
const handleCommitSelectedChanges = async () => {
if (selectedOutgoingChanges.length === 0) {
Alert.warning('Please select at least one change to commit.');
return;
}
if (!commitMessage.trim()) {
Alert.warning('Please enter a commit message.');
return;
}
const handleCommitSelectedChanges = async (selectedChanges, commitMessage) => {
setLoadingAction('commit_selected');
try {
const response = await pushFiles(
selectedOutgoingChanges,
commitMessage
);
const response = await pushFiles(selectedChanges, commitMessage);
if (response.success) {
await fetchGitStatus();
setSelectedOutgoingChanges([]); // Clear the selected changes after committing
setCommitMessage('');
Alert.success(response.message);
} else {
Alert.error(response.error);
}
} catch (error) {
Alert.error(
'An unexpected error occurred while committing changes.'
);
Alert.error('An unexpected error occurred while committing changes.');
console.error('Error committing changes:', error);
} finally {
setLoadingAction('');
}
};
const handleRevertSelectedChanges = async () => {
if (selectedOutgoingChanges.length === 0) {
Alert.warning('Please select at least one change to revert.');
return;
}
const handleRevertSelectedChanges = async (selectedChanges) => {
setLoadingAction('revert_selected');
try {
const response = await Promise.all(
selectedOutgoingChanges.map(filePath => revertFile(filePath))
);
const response = await Promise.all(selectedChanges.map(filePath => revertFile(filePath)));
const allSuccessful = response.every(res => res.success);
if (allSuccessful) {
await fetchGitStatus();
setSelectedOutgoingChanges([]); // Clear the selected changes after reverting
Alert.success(
'Selected changes have been reverted successfully.'
);
Alert.success('Selected changes have been reverted successfully.');
} else {
Alert.error(
'Some changes could not be reverted. Please try again.'
);
Alert.error('Some changes could not be reverted. Please try again.');
}
} catch (error) {
Alert.error(
'An unexpected error occurred while reverting changes.'
);
Alert.error('An unexpected error occurred while reverting changes.');
console.error('Error reverting changes:', error);
} finally {
setLoadingAction('');
}
};
const handlePullSelectedChanges = async () => {
if (selectedIncomingChanges.length === 0) {
Alert.warning('Please select at least one change to pull.');
return;
}
const handlePullSelectedChanges = async (selectedChanges) => {
setLoadingAction('pull_changes');
try {
// You would need to update your backend to handle pulling specific files
const response = await pullBranch(
status.branch,
selectedIncomingChanges
);
const response = await pullBranch(changes.branch, selectedChanges);
if (response.success) {
await fetchGitStatus();
setSelectedIncomingChanges([]); // Clear the selected changes after pulling
Alert.success(response.message);
} else {
Alert.error(response.error);
@@ -468,101 +153,11 @@ const SettingsPage = () => {
}
};
const handleSelectChange = (filePath, isIncoming) => {
if (isIncoming) {
setSelectedIncomingChanges(prevSelected => {
if (prevSelected.includes(filePath)) {
return prevSelected.filter(path => path !== filePath);
} else {
return [...prevSelected, filePath];
}
});
} else {
const change = status.outgoing_changes.find(
c => c.file_path === filePath
);
const isStaged = change.staged;
setSelectedOutgoingChanges(prevSelected => {
if (prevSelected.includes(filePath)) {
const newSelection = prevSelected.filter(
path => path !== filePath
);
if (newSelection.length === 0) setSelectionType(null);
return newSelection;
} else {
if (
prevSelected.length === 0 ||
(isStaged && selectionType === 'staged') ||
(!isStaged && selectionType === 'unstaged')
) {
setSelectionType(isStaged ? 'staged' : 'unstaged');
return [...prevSelected, filePath];
} else {
return prevSelected;
}
}
});
}
};
const loadingMessages = [
"Checking for changes... don't blink!",
'Syncing with the mothership...',
'Peeking under the hood...',
'Counting bits and bytes...',
'Scanning for modifications...',
'Looking for new stuff...',
'Comparing local and remote...',
"Checking your project's pulse...",
"Analyzing your code's mood...",
"Reading the project's diary..."
];
const getRandomLoadingMessage = () => {
return loadingMessages[
Math.floor(Math.random() * loadingMessages.length)
];
};
const getStatusIcon = status => {
switch (status) {
case 'Untracked':
return <Plus className='text-blue-400' size={16} />;
case 'Staged (New)':
return <Plus className='text-green-400' size={16} />;
case 'Staged (Modified)':
case 'Modified':
return <Edit className='text-yellow-400' size={16} />;
case 'Deleted':
return <MinusCircle className='text-red-400' size={16} />;
case 'Deleted (Staged)':
return <MinusCircle className='text-red-600' size={16} />;
case 'Renamed':
return <GitBranch className='text-purple-400' size={16} />;
default:
return <AlertCircle className='text-gray-400' size={16} />;
}
};
const getTypeIcon = type => {
switch (type) {
case 'Regex Pattern':
return <Code className='text-blue-400' size={16} />;
case 'Custom Format':
return <FileText className='text-green-400' size={16} />;
case 'Quality Profile':
return <Settings className='text-purple-400' size={16} />;
default:
return <File className='text-gray-400' size={16} />;
}
};
return (
<div>
<h2 className='text-xl font-bold mb-4 text-gray-100'>
Git Repository Settings
<span className='ml-4 text-sm text-gray-300 mb-4'>
<h2 className="text-xl font-bold mb-4 text-gray-100">
Git Settings
<span className="ml-4 text-sm text-gray-300 mb-4">
{isDevMode ? 'Dev Mode: Enabled' : 'Dev Mode: Disabled'}
</span>
</h2>
@@ -574,248 +169,48 @@ const SettingsPage = () => {
/>
{settings && (
<div className='space-y-4'>
<div className='bg-gray-700 p-4 rounded-md'>
<h3 className='text-sm font-semibold text-gray-100 mb-2'>
Git Status
</h3>
{loadingStatus ? (
<div className='flex items-center justify-center'>
<Loader
size={24}
className='animate-spin text-gray-300'
/>
<span className='ml-2 text-gray-300 text-sm'>
{loadingMessage}
</span>
</div>
) : (
status && (
<>
<div className='flex items-center justify-between mb-4'>
<div className='flex items-center'>
<GitBranch
className='mr-2 text-green-400'
size={14}
/>
<span className='text-gray-200 text-sm'>
Current Branch: {status.branch}
</span>
</div>
<button
onClick={() =>
setShowBranchModal(true)
}
className='flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-xs'>
<Eye size={14} className='mr-2' />
View Branches
</button>
</div>
{status.incoming_changes.length > 0 &&
renderChangeTable(
status.incoming_changes,
'Incoming Changes',
<ArrowDown
className='text-yellow-400 mr-2'
size={16}
/>,
true
)}
{status.outgoing_changes.length > 0 &&
renderChangeTable(
status.outgoing_changes,
'Outgoing Changes',
<ArrowUp
className='text-blue-400 mr-2'
size={16}
/>,
false
)}
<CommitSection
status={status}
commitMessage={commitMessage}
setCommitMessage={setCommitMessage}
loadingAction={loadingAction}
hasIncomingChanges={
status.incoming_changes.length > 0
}
funMessage={funMessage}
isDevMode={isDevMode}
/>
{/* Buttons Below Commit Section */}
<div className='mt-4 flex justify-end space-x-2'>
{isDevMode && (
<>
{/* Stage button */}
{selectedOutgoingChanges.length >
0 &&
selectionType !==
'staged' && (
<Tooltip
content={getStageButtonTooltip()}>
<button
onClick={
handleStageSelectedChanges
}
className='flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors duration-200 ease-in-out text-xs'
disabled={
loadingAction ===
'stage_selected'
}>
{loadingAction ===
'stage_selected' ? (
<Loader
size={
12
}
className='animate-spin'
/>
) : (
<Plus
className='mr-1'
size={
12
}
/>
)}
Stage Selected
</button>
</Tooltip>
)}
{/* Commit button */}
{selectedOutgoingChanges.length >
0 &&
commitMessage.trim() &&
selectionType !==
'unstaged' && (
<Tooltip
content={getCommitButtonTooltip()}>
<button
onClick={
handleCommitSelectedChanges
}
className='flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-xs'
disabled={
loadingAction ===
'commit_selected'
}>
{loadingAction ===
'commit_selected' ? (
<Loader
size={
12
}
className='animate-spin'
/>
) : (
<CheckCircle
className='mr-1'
size={
12
}
/>
)}
Commit Selected
</button>
</Tooltip>
)}
</>
)}
{/* Revert button (moved outside isDevMode check) */}
{selectedOutgoingChanges.length > 0 && (
<Tooltip
content={getRevertButtonTooltip()}>
<button
onClick={
handleRevertSelectedChanges
}
className='flex items-center px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors duration-200 ease-in-out text-xs'
disabled={
loadingAction ===
'revert_selected'
}>
{loadingAction ===
'revert_selected' ? (
<Loader
size={12}
className='animate-spin'
/>
) : (
<RotateCcw
className='mr-1'
size={12}
/>
)}
Revert Selected
</button>
</Tooltip>
)}
{/* Pull button (always enabled) */}
{selectedIncomingChanges.length > 0 && (
<Tooltip content='Pull selected changes'>
<button
onClick={
handlePullSelectedChanges
}
className='flex items-center px-4 py-2 bg-yellow-600 text-white rounded-md hover:bg-yellow-700 transition-colors duration-200 ease-in-out text-xs'
disabled={
loadingAction ===
'pull_changes'
}>
{loadingAction ===
'pull_changes' ? (
<Loader
size={12}
className='animate-spin'
/>
) : (
<Download
className='mr-1'
size={12}
/>
)}
Pull Selected
</button>
</Tooltip>
)}
</div>
</>
)
)}
</div>
<div className="space-y-4">
{statusLoading ? (
<div className="flex items-left justify-left dark:bg-gray-800 p-4 rounded-md border border-gray-200 dark:border-gray-700 text-sm">
<Loader className="animate-spin mr-2" size={16} />
<span className="text-gray-300">{statusLoadingMessage}</span>
</div>
) : changes && (changes.incoming_changes.length > 0 || changes.outgoing_changes.length > 0) ? (
<StatusContainer
status={changes}
isDevMode={isDevMode}
onViewBranches={() => setShowBranchModal(true)}
onStageSelected={handleStageSelectedChanges}
onCommitSelected={handleCommitSelectedChanges}
onRevertSelected={handleRevertSelectedChanges}
onPullSelected={handlePullSelectedChanges}
loadingAction={loadingAction}
/>
) : (
<div className="dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-4 rounded-md text-gray-300 text-left text-sm">
{noChangesMessage}
</div>
)}
</div>
)}
<h2 className='text-xl font-bold mb-4 text-gray-100 mt-3'>
<h2 className="text-xl font-bold mb-4 text-gray-100 mt-3">
Arr Management
</h2>
<ArrContainer />
{settings && status && (
{settings && changes && (
<SettingsBranchModal
isOpen={showBranchModal}
onClose={() => setShowBranchModal(false)}
repoUrl={settings.gitRepo}
currentBranch={status.branch}
currentBranch={changes.branch}
onBranchChange={fetchGitStatus}
isDevMode={isDevMode}
/>
)}
{showDiffModal && currentChange && (
<DiffModal
isOpen={showDiffModal}
onClose={() => setShowDiffModal(false)}
diffContent={diffContent}
type={currentChange.type}
name={currentChange.name}
commitMessage={currentChange.commit_message}
isDevMode={isDevMode}
/>
)}
</div>
);
};
export default SettingsPage;
export default SettingsPage;

View File

@@ -0,0 +1,148 @@
import React, {useState} from 'react';
import {
Eye,
Loader,
Plus,
MinusCircle,
Edit,
GitBranch,
AlertCircle,
Code,
FileText,
Settings,
File
} from 'lucide-react';
import Tooltip from '../../ui/Tooltip';
import {getDiff} from '../../../api/api';
import Alert from '../../ui/Alert';
import Diff from './modal/ViewDiff';
const ChangeRow = ({change, isSelected, onSelect, isIncoming, isDevMode}) => {
const [loadingDiff, setLoadingDiff] = useState(false);
const [showDiff, setShowDiff] = useState(false);
const [diffContent, setDiffContent] = useState('');
const getStatusIcon = status => {
switch (status) {
case 'Untracked':
return <Plus className='text-blue-400' size={16} />;
case 'Staged (New)':
return <Plus className='text-green-400' size={16} />;
case 'Staged (Modified)':
case 'Modified':
return <Edit className='text-yellow-400' size={16} />;
case 'Deleted':
return <MinusCircle className='text-red-400' size={16} />;
case 'Deleted (Staged)':
return <MinusCircle className='text-red-600' size={16} />;
case 'Renamed':
return <GitBranch className='text-purple-400' size={16} />;
default:
return <AlertCircle className='text-gray-400' size={16} />;
}
};
const getTypeIcon = type => {
switch (type) {
case 'Regex Pattern':
return <Code className='text-blue-400' size={16} />;
case 'Custom Format':
return <FileText className='text-green-400' size={16} />;
case 'Quality Profile':
return <Settings className='text-purple-400' size={16} />;
default:
return <File className='text-gray-400' size={16} />;
}
};
const handleViewDiff = async () => {
setLoadingDiff(true);
try {
const response = await getDiff(change.file_path);
if (response.success) {
setDiffContent(response.diff);
setShowDiff(true);
} else {
Alert.error(response.error);
}
} catch (error) {
Alert.error(
'An unexpected error occurred while fetching the diff.'
);
console.error('Error fetching diff:', error);
} finally {
setLoadingDiff(false);
}
};
return (
<>
<tr
className={`border-t border-gray-600 cursor-pointer hover:bg-gray-700 ${
isSelected ? 'bg-gray-700' : ''
}`}
onClick={() => onSelect(change.file_path)}>
<td className='px-4 py-2 text-gray-300'>
<div className='flex items-center'>
{getStatusIcon(change.status)}
<span className='ml-2'>
{change.staged
? `${change.status} (Staged)`
: change.status}
</span>
</div>
</td>
<td className='px-4 py-2 text-gray-300'>
<div className='flex items-center'>
{getTypeIcon(change.type)}
<span className='ml-2'>{change.type}</span>
</div>
</td>
<td className='px-4 py-2 text-gray-300'>
{change.name || 'Unnamed'}
</td>
<td className='px-4 py-2 text-left align-middle'>
<Tooltip content='View differences'>
<button
onClick={e => {
e.stopPropagation();
handleViewDiff();
}}
className='flex items-center justify-center px-2 py-1 bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors text-xs'
style={{width: '100%'}}>
{loadingDiff ? (
<Loader size={12} className='animate-spin' />
) : (
<>
<Eye size={12} className='mr-1' />
View Diff
</>
)}
</button>
</Tooltip>
</td>
<td className='px-4 py-2 text-right text-gray-300 align-middle'>
<input
type='checkbox'
checked={isSelected}
onChange={e => e.stopPropagation()}
disabled={!isIncoming && change.staged}
/>
</td>
</tr>
{showDiff && (
<Diff
isOpen={showDiff}
onClose={() => setShowDiff(false)}
diffContent={diffContent}
type={change.type}
name={change.name}
commitMessage={change.commit_message}
isDevMode={isDevMode}
/>
)}
</>
);
};
export default ChangeRow;

View File

@@ -0,0 +1,103 @@
import React from 'react';
import {ArrowDown, ArrowUp} from 'lucide-react';
import ChangeRow from './ChangeRow';
const ChangeTable = ({
changes,
title,
icon,
isIncoming,
selectedChanges,
onSelectChange,
sortConfig,
onRequestSort,
isDevMode
}) => {
const sortedChanges = changesArray => {
if (!sortConfig.key) return changesArray;
return [...changesArray].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? 1 : -1;
}
return 0;
});
};
const SortableHeader = ({children, sortKey}) => {
const isSorted = sortConfig.key === sortKey;
return (
<th
className='px-4 py-2 text-left text-gray-300 cursor-pointer hover:bg-gray-500'
onClick={() => onRequestSort(sortKey)}>
<div className='flex items-center'>
{children}
{isSorted &&
(sortConfig.direction === 'ascending' ? (
<ArrowUp size={14} className='ml-1' />
) : (
<ArrowDown size={14} className='ml-1' />
))}
</div>
</th>
);
};
return (
<div className='mb-4'>
<h4 className='text-sm font-medium text-gray-200 mb-2 flex items-center'>
{icon}
<span>
{isIncoming
? title
: isDevMode
? 'Outgoing Changes'
: 'Local Changes'}{' '}
({changes.length})
</span>
</h4>
<div className='border border-gray-600 rounded-md overflow-hidden'>
<table className='w-full text-sm'>
<thead className='bg-gray-600'>
<tr>
<SortableHeader sortKey='status'>
Status
</SortableHeader>
<SortableHeader sortKey='type'>Type</SortableHeader>
<SortableHeader sortKey='name'>Name</SortableHeader>
<th className='px-4 py-2 text-left text-gray-300 w-1/5'>
Actions
</th>
<th className='px-4 py-2 text-right text-gray-300 w-1/10'>
Select
</th>
</tr>
</thead>
<tbody>
{sortedChanges(changes).map((change, index) => (
<ChangeRow
key={`${
isIncoming ? 'incoming' : 'outgoing'
}-${index}`}
change={change}
isSelected={selectedChanges.includes(
change.file_path
)}
onSelect={filePath =>
onSelectChange(filePath, isIncoming)
}
isIncoming={isIncoming}
isDevMode={isDevMode}
/>
))}
</tbody>
</table>
</div>
</div>
);
};
export default ChangeTable;

View File

@@ -1,22 +1,22 @@
import React, {useState} from 'react';
import {Loader, Unlink, Link} from 'lucide-react';
import React, { useState } from 'react';
import { Loader, Unlink, Link } from 'lucide-react';
import Tooltip from '../../ui/Tooltip';
import {unlinkRepo, getSettings} from '../../../api/api';
import { unlinkRepo, getSettings } from '../../../api/api';
import Alert from '../../ui/Alert';
import LinkRepo from './modal/LinkRepo';
import UnlinkRepo from './modal/UnlinkRepo';
const RepoContainer = ({settings, setSettings, fetchGitStatus}) => {
const RepoContainer = ({ settings, setSettings, fetchGitStatus }) => {
const [loadingAction, setLoadingAction] = useState('');
const [showLinkModal, setShowLinkModal] = useState(false);
const [showUnlinkRepo, setShowUnlinkRepo] = useState(false);
const handleLinkRepo = async () => {
const handleLinkRepo = () => {
setLoadingAction('link_repo');
setShowLinkModal(true);
};
const handleUnlinkRepo = async removeFiles => {
const handleUnlinkRepo = async (removeFiles) => {
setLoadingAction('unlink_repo');
try {
const response = await unlinkRepo(removeFiles);
@@ -47,23 +47,17 @@ const RepoContainer = ({settings, setSettings, fetchGitStatus}) => {
}
};
if (!settings) {
return (
<>
<button
onClick={handleLinkRepo}
className='flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-sm font-medium shadow-md'>
<Link size={16} className='mr-2' />
Link Repository
</button>
<LinkRepo
isOpen={showLinkModal}
onClose={() => setShowLinkModal(false)}
onSubmit={onLinkSubmit}
/>
</>
);
}
// Handler to close the LinkRepo modal and reset loadingAction
const closeLinkModal = () => {
setShowLinkModal(false);
setLoadingAction('');
};
// Handler to close the UnlinkRepo modal and reset loadingAction
const closeUnlinkModal = () => {
setShowUnlinkRepo(false);
setLoadingAction('');
};
return (
<div className='space-y-4 mb-4'>
@@ -71,37 +65,43 @@ const RepoContainer = ({settings, setSettings, fetchGitStatus}) => {
<div className='flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-3 sm:space-y-0'>
<div className='flex flex-col sm:flex-row sm:items-center'>
<h3 className='text-sm font-semibold text-gray-100 mr-2 mb-1 sm:mb-0'>
Connected Repository:
{settings ? 'Connected Repository:' : 'Repository:'}
</h3>
<a
href={settings.gitRepo}
target='_blank'
rel='noopener noreferrer'
className='text-blue-400 hover:text-blue-300 transition-colors text-sm font-medium truncate max-w-xs sm:max-w-md'>
{settings.gitRepo}
</a>
{settings ? (
<a
href={settings.gitRepo}
target='_blank'
rel='noopener noreferrer'
className='text-blue-400 hover:text-blue-300 transition-colors text-sm font-medium truncate max-w-xs sm:max-w-md'>
{settings.gitRepo}
</a>
) : (
<span className='text-gray-400 text-sm'>No repository linked</span>
)}
</div>
<Tooltip content='Unlink Repository'>
<Tooltip content={settings ? 'Unlink Repository' : 'Link Repository'}>
<button
onClick={() => setShowUnlinkRepo(true)}
className='flex items-center px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors duration-200 ease-in-out text-sm font-medium shadow-sm'
disabled={loadingAction === 'unlink_repo'}>
{loadingAction === 'unlink_repo' ? (
<Loader
size={16}
className='animate-spin mr-2'
/>
) : (
onClick={settings ? () => setShowUnlinkRepo(true) : handleLinkRepo}
className={`flex items-center px-4 py-2 ${settings ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'} text-white rounded-md transition-colors duration-200 ease-in-out text-sm font-medium shadow-sm`}
disabled={loadingAction !== ''}>
{settings ? (
<Unlink size={16} className='mr-2' />
) : (
<Link size={16} className='mr-2' />
)}
Unlink
{settings ? 'Unlink' : 'Link Repository'}
</button>
</Tooltip>
</div>
</div>
<LinkRepo
isOpen={showLinkModal}
onClose={closeLinkModal}
onSubmit={onLinkSubmit}
/>
<UnlinkRepo
isOpen={showUnlinkRepo}
onClose={() => setShowUnlinkRepo(false)}
onClose={closeUnlinkModal}
onSubmit={handleUnlinkRepo}
/>
</div>

View File

@@ -0,0 +1,274 @@
import React, {useState} from 'react';
import {
GitBranch,
Loader,
RotateCcw,
Download,
CheckCircle,
Plus,
Eye
} from 'lucide-react';
import ChangeTable from './ChangeTable';
import Tooltip from '../../ui/Tooltip';
import CommitSection from '../CommitSection';
const StatusContainer = ({
status,
isDevMode,
onViewBranches,
onStageSelected,
onCommitSelected,
onRevertSelected,
onPullSelected,
loadingAction
}) => {
const [sortConfig, setSortConfig] = useState({
key: 'type',
direction: 'ascending'
});
const [selectedIncomingChanges, setSelectedIncomingChanges] = useState([]);
const [selectedOutgoingChanges, setSelectedOutgoingChanges] = useState([]);
const [commitMessage, setCommitMessage] = useState('');
const [selectionType, setSelectionType] = useState(null);
const requestSort = key => {
let direction = 'ascending';
if (sortConfig.key === key && sortConfig.direction === 'ascending') {
direction = 'descending';
}
setSortConfig({key, direction});
};
const handleSelectChange = (filePath, isIncoming) => {
if (isIncoming) {
setSelectedIncomingChanges(prevSelected => {
if (prevSelected.includes(filePath)) {
return prevSelected.filter(path => path !== filePath);
} else {
return [...prevSelected, filePath];
}
});
} else {
const change = status.outgoing_changes.find(
c => c.file_path === filePath
);
const isStaged = change.staged;
setSelectedOutgoingChanges(prevSelected => {
if (prevSelected.includes(filePath)) {
const newSelection = prevSelected.filter(
path => path !== filePath
);
if (newSelection.length === 0) setSelectionType(null);
return newSelection;
} else {
if (
prevSelected.length === 0 ||
(isStaged && selectionType === 'staged') ||
(!isStaged && selectionType === 'unstaged')
) {
setSelectionType(isStaged ? 'staged' : 'unstaged');
return [...prevSelected, filePath];
} else {
return prevSelected;
}
}
});
}
};
const getStageButtonTooltip = () => {
if (selectionType === 'staged') {
return 'These files are already staged';
}
if (selectedOutgoingChanges.length === 0) {
return 'Select files to stage';
}
return 'Stage selected files';
};
const getCommitButtonTooltip = () => {
if (selectionType === 'unstaged') {
return 'You can only commit staged files';
}
if (selectedOutgoingChanges.length === 0) {
return 'Select files to commit';
}
if (!commitMessage.trim()) {
return 'Enter a commit message';
}
return 'Commit selected files';
};
const getRevertButtonTooltip = () => {
if (selectedOutgoingChanges.length === 0) {
return 'Select files to revert';
}
return 'Revert selected files';
};
return (
<div className='dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-4 rounded-md'>
<h3 className='text-sm font-semibold text-gray-100 mb-2'>
Git Status
</h3>
<div className='flex items-center justify-between mb-4'>
<div className='flex items-center'>
<GitBranch className='mr-2 text-green-400' size={14} />
<span className='text-gray-200 text-sm'>
Current Branch: {status.branch}
</span>
</div>
<button
onClick={onViewBranches}
className='flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-xs'>
<Eye size={14} className='mr-2' />
View Branches
</button>
</div>
{status.incoming_changes.length > 0 && (
<ChangeTable
changes={status.incoming_changes}
title='Incoming Changes'
icon={
<Download className='text-yellow-400 mr-2' size={16} />
}
isIncoming={true}
selectedChanges={selectedIncomingChanges}
onSelectChange={handleSelectChange}
sortConfig={sortConfig}
onRequestSort={requestSort}
isDevMode={isDevMode}
/>
)}
{status.outgoing_changes.length > 0 && (
<ChangeTable
changes={status.outgoing_changes}
title='Outgoing Changes'
icon={
<GitBranch className='text-blue-400 mr-2' size={16} />
}
isIncoming={false}
selectedChanges={selectedOutgoingChanges}
onSelectChange={handleSelectChange}
sortConfig={sortConfig}
onRequestSort={requestSort}
isDevMode={isDevMode}
/>
)}
<CommitSection
status={status}
commitMessage={commitMessage}
setCommitMessage={setCommitMessage}
loadingAction={loadingAction}
hasIncomingChanges={status.incoming_changes.length > 0}
isDevMode={isDevMode}
/>
{/* Action Buttons */}
<div className='mt-4 flex justify-end space-x-2'>
{isDevMode && (
<>
{selectedOutgoingChanges.length > 0 &&
selectionType !== 'staged' && (
<Tooltip content={getStageButtonTooltip()}>
<button
onClick={() =>
onStageSelected(
selectedOutgoingChanges
)
}
className='flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors duration-200 ease-in-out text-xs'
disabled={
loadingAction === 'stage_selected'
}>
{loadingAction === 'stage_selected' ? (
<Loader
size={12}
className='animate-spin'
/>
) : (
<Plus className='mr-1' size={12} />
)}
Stage Selected
</button>
</Tooltip>
)}
{selectedOutgoingChanges.length > 0 &&
commitMessage.trim() &&
selectionType !== 'unstaged' && (
<Tooltip content={getCommitButtonTooltip()}>
<button
onClick={() =>
onCommitSelected(
selectedOutgoingChanges,
commitMessage
)
}
className='flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-xs'
disabled={
loadingAction === 'commit_selected'
}>
{loadingAction === 'commit_selected' ? (
<Loader
size={12}
className='animate-spin'
/>
) : (
<CheckCircle
className='mr-1'
size={12}
/>
)}
Commit Selected
</button>
</Tooltip>
)}
</>
)}
{selectedOutgoingChanges.length > 0 && (
<Tooltip content={getRevertButtonTooltip()}>
<button
onClick={() =>
onRevertSelected(selectedOutgoingChanges)
}
className='flex items-center px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors duration-200 ease-in-out text-xs'
disabled={loadingAction === 'revert_selected'}>
{loadingAction === 'revert_selected' ? (
<Loader size={12} className='animate-spin' />
) : (
<RotateCcw className='mr-1' size={12} />
)}
Revert Selected
</button>
</Tooltip>
)}
{selectedIncomingChanges.length > 0 && (
<Tooltip content='Pull selected changes'>
<button
onClick={() =>
onPullSelected(selectedIncomingChanges)
}
className='flex items-center px-4 py-2 bg-yellow-600 text-white rounded-md hover:bg-yellow-700 transition-colors duration-200 ease-in-out text-xs'
disabled={loadingAction === 'pull_changes'}>
{loadingAction === 'pull_changes' ? (
<Loader size={12} className='animate-spin' />
) : (
<Download className='mr-1' size={12} />
)}
Pull Selected
</button>
</Tooltip>
)}
</div>
</div>
);
};
export default StatusContainer;

View File

@@ -1,8 +1,8 @@
import React, {useState} from 'react';
import Modal from '../../ui/Modal';
import Modal from '../../../ui/Modal';
import {Loader} from 'lucide-react';
import {cloneRepo} from '../../../api/api';
import Alert from '../../ui/Alert';
import {cloneRepo} from '../../../../api/api';
import Alert from '../../../ui/Alert';
const LinkRepo = ({isOpen, onClose, onSubmit}) => {
const [gitRepo, setGitRepo] = useState('');

View File

@@ -0,0 +1,100 @@
import React from 'react';
import PropTypes from 'prop-types';
import Modal from '../../../ui/Modal';
const Diff = ({
isOpen,
onClose,
diffContent,
type,
name,
commitMessage,
title = 'View Diff'
}) => {
const formatDiffContent = content => {
if (!content) return [];
return content.split('\n').map((line, index) => {
let lineClass = 'py-1 pl-4 border-l-2 ';
if (line.startsWith('+')) {
lineClass += 'bg-green-900/30 text-green-400 border-green-500';
} else if (line.startsWith('-')) {
lineClass += 'bg-red-900/30 text-red-400 border-red-500';
} else {
lineClass += 'border-transparent';
}
return (
<div key={index} className={`flex ${lineClass}`}>
<span className='w-12 text-gray-500 select-none text-right pr-4 border-r border-gray-700'>
{index + 1}
</span>
<code className='flex-1 pl-4 font-mono text-sm'>
{line}
</code>
</div>
);
});
};
const formattedContent = formatDiffContent(diffContent);
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} size='4xl'>
<div className='space-y-4'>
<div className='bg-gray-100 dark:bg-gray-700 p-4 rounded-lg space-y-2 text-sm'>
<div className='flex justify-between items-center'>
<span className='font-medium text-gray-600 dark:text-gray-300'>
Type:
</span>
<span className='bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded'>
{type}
</span>
</div>
<div className='flex justify-between items-center'>
<span className='font-medium text-gray-600 dark:text-gray-300'>
Name:
</span>
<span className='bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 px-2 py-1 rounded'>
{name === 'Deleted File' ? 'Deleted File' : name}
</span>
</div>
{commitMessage && (
<div className='flex flex-col'>
<span className='font-medium text-gray-600 dark:text-gray-300 mb-1'>
Commit Message:
</span>
<p className='bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 p-2 rounded'>
{commitMessage}
</p>
</div>
)}
</div>
<div className='border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden'>
<div className='bg-gray-50 dark:bg-gray-800 p-2 text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600'>
Diff Content
</div>
<div className='bg-white dark:bg-gray-900 p-4 max-h-[60vh] overflow-y-auto'>
{formattedContent.length > 0 ? (
formattedContent
) : (
<div className='text-gray-500 dark:text-gray-400 italic'>
No differences found or file is empty.
</div>
)}
</div>
</div>
</div>
</Modal>
);
};
Diff.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
diffContent: PropTypes.string,
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
commitMessage: PropTypes.string,
title: PropTypes.string
};
export default Diff;

View File

@@ -0,0 +1,31 @@
// messages.js
export const statusLoadingMessages = [
"Checking for changes... don't blink!",
'Syncing with the mothership...',
'Peeking under the hood...',
'Counting bits and bytes...',
'Scanning for modifications...',
'Looking for new stuff...',
'Comparing local and remote...',
"Checking your project's pulse...",
"Analyzing your code's mood...",
"Reading the project's diary..."
];
export const noChangesMessages = [
'No changes detected. Your regex is so precise, it could find a needle in a haystack... made of needles. 🧵🔍',
'All quiet on the commit front. Your custom formats are so perfect, even perfectionists are jealous. 🏆',
"No updates needed. Your media automation is running so smoothly, it's making butter jealous. 🧈",
'Zero modifications. Your torrent setup is seeding so efficiently, farmers are asking for advice. 🌾',
"No edits required. Your regex fu is so strong, it's bench-pressing parentheses for fun. 💪()",
'Unchanged status. Your Plex library is so well-organized, librarians are taking notes. 📚🤓',
"No alterations found. Your file naming scheme is so consistent, it's bringing tears to OCD eyes. 😢👀",
"All systems nominal. Your download queue is so orderly, it's making Marie Kondo question her career. 🧹✨",
"No revisions necessary. Your automation scripts are so smart, they're solving captchas for fun. 🤖🧩",
'Steady as she goes. Your media collection is so complete, Netflix is asking you for recommendations. 🎬👑'
];
export const getRandomMessage = (messages) => {
return messages[Math.floor(Math.random() * messages.length)];
};