Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/CHANGELOG.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@


releases:
- name: 3.6.0
changes:
- title: Add Quality Assurance functionality
categories: [Core]
authors: [FoxtotSierra]
- name: 3.5.2
changes:
- title: Fixed "Don't show this again" toggle button
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fbw-installer",
"productName": "FlyByWire Installer",
"version": "3.5.2-dev.1",
"version": "3.6.0-dev.1",
"description": "Desktop application to install and customize FlyByWire addons",
"configUrls": {
"production": "https://flybywirecdn.com/installer/config/production.json",
Expand Down
133 changes: 130 additions & 3 deletions src/renderer/components/AddonSection/Configure/TrackSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { InstallerStore } from 'renderer/redux/store';
import { Check } from 'tabler-icons-react';
import { Check, ChevronDown } from 'tabler-icons-react';
import { Addon, AddonTrack } from 'renderer/utils/InstallerConfiguration';
import cn from 'renderer/utils/cn';
import '../index.css';
Expand All @@ -11,7 +11,6 @@ export const Tracks: React.FC = ({ children }) => (
);

type TrackProps = {
className?: string;
addon: Addon;
track: AddonTrack;
isSelected: boolean;
Expand Down Expand Up @@ -42,3 +41,131 @@ export const Track: React.FC<TrackProps> = ({ isSelected, isInstalled, handleSel
</div>
);
};

type QATrackSelectorProps = {
addon: Addon;
tracks: AddonTrack[];
selectedTrack: AddonTrack | null;
installedTrack: AddonTrack | null;
onTrackSelection: (track: AddonTrack) => void;
};

export const QATrackSelector: React.FC<QATrackSelectorProps> = ({
addon,
tracks,
selectedTrack,
installedTrack,
onTrackSelection,
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

const selectedQATrack = tracks.find((track) => track.key === selectedTrack?.key);
const latestVersionName = useSelector<InstallerStore, string | undefined>(
(state) => state.latestVersionNames[addon.key]?.[selectedQATrack?.key]?.name,
);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

const handleTrackSelect = (track: AddonTrack) => {
onTrackSelection(track);
setIsOpen(false);
};

return (
<div className="relative w-max min-w-[400px] max-w-[650px]" ref={dropdownRef}>
<div
className={cn(
'flex w-full h-24 cursor-pointer items-center justify-between rounded-sm-md border-2 border-transparent bg-navy-dark text-white transition-all duration-200 hover:border-navy-lightest hover:text-gray-300 px-4',
selectedQATrack && 'border-2 border-cyan text-cyan',
)}
onClick={() => setIsOpen(!isOpen)}
>
<div className="flex min-w-0 flex-col px-3 py-2.5">
<span className="truncate text-xl text-current">
{selectedQATrack ? selectedQATrack.name : 'Select QA Build'}
</span>
{selectedQATrack && (
<span className="mt-0.5 flex justify-between font-manrope text-3xl font-medium tracking-wider text-current">
{latestVersionName ?? <span className="mt-1.5 block h-7 w-32 animate-pulse bg-navy-light"></span>}
</span>
)}
</div>

<div className="flex min-w-24 items-center justify-end gap-2">
{installedTrack === selectedQATrack && <Check className="stroke-current text-cyan" strokeWidth={3} />}
<ChevronDown
className={cn('stroke-current transition-transform duration-200 text-white', isOpen && 'rotate-180')}
strokeWidth={2}
/>
</div>
</div>

{isOpen && (
<div className="absolute inset-x-0 top-full z-10 mt-2 max-h-72 overflow-y-auto rounded-sm-md border border-navy-lightest bg-navy-dark shadow-lg">
{tracks
.slice()
.sort((a, b) => b.key.localeCompare(a.key, undefined, { numeric: true }))
.map((track) => (
<QATrackDropdownItem
key={track.key}
addon={addon}
track={track}
isSelected={track.key === selectedTrack?.key}
isInstalled={track.key === installedTrack?.key}
onSelect={() => handleTrackSelect(track)}
/>
))}
</div>
)}
</div>
);
};

type QATrackDropdownItemProps = {
addon: Addon;
track: AddonTrack;
isSelected: boolean;
isInstalled: boolean;
onSelect: () => void;
};

const QATrackDropdownItem: React.FC<QATrackDropdownItemProps> = ({
addon,
track,
isSelected,
isInstalled,
onSelect,
}) => {
const latestVersionName = useSelector<InstallerStore, string | undefined>(
(state) => state.latestVersionNames[addon.key]?.[track.key]?.name,
);

return (
<div
className={cn(
'flex items-center justify-between px-4 py-3 cursor-pointer text-white hover:bg-navy-light transition-colors duration-150',
isSelected && 'text-cyan',
)}
onClick={onSelect}
>
<div className="flex min-w-0 flex-col">
<span className="truncate text-nowrap text-lg text-current">{track.name}</span>
<span className={cn('font-manrope text-sm text-white', isSelected && 'text-cyan')}>
{latestVersionName ?? 'Loading...'}
</span>
</div>

{isInstalled && <Check className="stroke-current text-cyan" strokeWidth={3} />}
</div>
);
};
22 changes: 18 additions & 4 deletions src/renderer/components/AddonSection/Configure/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import { Addon, AddonTrack, ConfigurationAspect } from 'renderer/utils/InstallerConfiguration';
import { Track, Tracks } from './TrackSelector';
import { QATrackSelector, Track, Tracks } from './TrackSelector';
import { ConfigurationAspectDisplay } from 'renderer/components/AddonSection/Configure/ConfigurationAspectDisplay';

import './index.css';
Expand Down Expand Up @@ -35,7 +35,7 @@ export const Configure: FC<ConfigureProps> = ({
<div>
<Tracks>
{selectedAddon.tracks
.filter((track) => !track.isExperimental)
.filter((track) => !track.isExperimental && !track.isQualityAssurance)
.map((track) => (
<Track
addon={selectedAddon}
Expand All @@ -52,7 +52,7 @@ export const Configure: FC<ConfigureProps> = ({
<div>
<Tracks>
{selectedAddon.tracks
.filter((track) => track.isExperimental)
.filter((track) => track.isExperimental && !track.isQualityAssurance)
.map((track) => (
<Track
addon={selectedAddon}
Expand All @@ -65,11 +65,25 @@ export const Configure: FC<ConfigureProps> = ({
))}
</Tracks>

{selectedAddon.tracks.filter((track) => track.isExperimental).length > 0 && (
{selectedAddon.tracks.some((track) => track.isExperimental) && (
<span className="ml-0.5 mt-3 inline-block text-2xl text-quasi-white">Experimental versions</span>
)}
</div>
</div>
{selectedAddon.tracks.some((track) => track.isQualityAssurance) && (
<div className="mt-8 flex flex-row gap-x-8">
<div className="w-full">
<QATrackSelector
addon={selectedAddon}
tracks={selectedAddon.tracks.filter((track) => track.isQualityAssurance)}
selectedTrack={selectedTrack}
installedTrack={installedTrack}
onTrackSelection={onTrackSelection}
/>
<span className="ml-0.5 mt-3 inline-block text-2xl text-quasi-white">Quality Assurance</span>
</div>
</div>
)}
{selectedTrack && selectedTrack.description && (
<div className="mt-10">
<h2 className="font-bold text-white">Description</h2>
Expand Down
108 changes: 91 additions & 17 deletions src/renderer/components/SettingsSection/Developer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,45 @@ const SettingsItem: FC<{ name: string }> = ({ name, children }) => (
export const DeveloperSettings: React.FC = () => {
const [configDownloadUrl, setConfigDownloadUrl] = useSetting<string>('mainSettings.configDownloadUrl');
const [configDownloadUrlValid, setConfigDownloadUrlValid] = useState<boolean>(false);
const [qaConfigUrls, setQaConfigUrls] = useSetting<Record<number, string>>('mainSettings.qaConfigUrls');
const [qaConfigUrlsValid, setQaConfigUrlsValid] = useState<Record<number, boolean>>({});

const [configForceUseLocal, setConfigForceUseLocal] = useSetting<boolean>('mainSettings.configForceUseLocal');

const validateUrl = useCallback(() => {
try {
fetch(configDownloadUrl).then((response) => {
setConfigDownloadUrlValid(response.status === 200);
});
} catch (e) {
const validateUrls = useCallback(() => {
// Validate main config URL
if (configDownloadUrl) {
fetch(configDownloadUrl)
.then((response) => {
setConfigDownloadUrlValid(response.status === 200);
})
.catch(() => {
setConfigDownloadUrlValid(false);
});
} else {
setConfigDownloadUrlValid(false);
}
}, [configDownloadUrl]);

// Validate QA config URLs
qaConfigUrls &&
Object.entries(qaConfigUrls).forEach(([key, url]) => {
if (url) {
fetch(url)
.then((response) => {
setQaConfigUrlsValid((prev) => ({ ...prev, [Number(key)]: response.status === 200 }));
})
.catch(() => {
setQaConfigUrlsValid((prev) => ({ ...prev, [Number(key)]: false }));
});
} else {
setQaConfigUrlsValid((prev) => ({ ...prev, [Number(key)]: false }));
}
});
}, [configDownloadUrl, qaConfigUrls]);

useEffect(() => {
validateUrl();
}, [validateUrl]);
validateUrls();
}, [validateUrls]);

return (
<div>
Expand All @@ -45,16 +68,9 @@ export const DeveloperSettings: React.FC = () => {
value={configDownloadUrl}
type="url"
onChange={(event) => setConfigDownloadUrl(event.target.value)}
onBlur={() => validateUrl()}
onBlur={() => validateUrls()}
size={50}
/>
<Button
type={ButtonType.Neutral}
className="ml-2 h-fit min-h-10 text-lg"
onClick={() => ipcRenderer.send(channels.window.reload)}
>
Reload Installer
</Button>
<Button
type={ButtonType.Neutral}
className="ml-2 h-fit min-h-10 text-lg"
Expand All @@ -64,11 +80,69 @@ export const DeveloperSettings: React.FC = () => {
</Button>
</div>
</SettingsItem>
<SettingsItem name="QA Configuration URLs">
<div className="flex flex-col gap-2 py-2 text-white">
{Object.entries(qaConfigUrls).map(([key, url]) => (
<div key={key} className="flex flex-row items-center gap-2">
<span className="w-8 text-center text-sm text-gray-400">#{key}</span>
<input
className={` text-right text-xl ${qaConfigUrlsValid[Number(key)] ? 'text-green-500' : 'text-red-500'}`}
value={url}
type="url"
onChange={(event) => setQaConfigUrls({ ...qaConfigUrls, [Number(key)]: event.target.value })}
size={40}
/>
<Button
type={ButtonType.Neutral}
className="h-fit min-h-10 px-2 text-lg"
onClick={() => {
const newUrls = { ...qaConfigUrls };
delete newUrls[Number(key)];
setQaConfigUrls(newUrls);
}}
>
Remove
</Button>
</div>
))}
<div className="flex flex-row items-center gap-2">
<Button
type={ButtonType.Neutral}
className="h-fit min-h-10 px-3 text-lg"
onClick={() => {
const nextKey = Math.max(0, ...Object.keys(qaConfigUrls).map(Number)) + 1;
setQaConfigUrls({ ...qaConfigUrls, [nextKey]: '' });
}}
>
Add URL
</Button>
<Button
type={ButtonType.Neutral}
className="h-fit min-h-10 px-3 text-lg"
onClick={() => setQaConfigUrls({})}
>
Clear All
</Button>
</div>
</div>
</SettingsItem>
<SettingsItem name="Force Use Local Configuration">
<div className="flex flex-row items-center justify-between text-white">
<Toggle value={configForceUseLocal} onToggle={setConfigForceUseLocal} />
</div>
</SettingsItem>
<SettingsItem name="">
<div className="flex w-full flex-row items-center justify-between text-white">
<span className="text-sm text-red-500">All changes on this page require a reload to take effect</span>
<Button
type={ButtonType.Neutral}
className="ml-2 h-fit min-h-10 text-lg"
onClick={() => ipcRenderer.send(channels.window.reload)}
>
Reload Installer
</Button>
</div>
</SettingsItem>
</div>
</div>
</div>
Expand Down
Loading