Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add possibility of getting latest track versions from fragmenter distribution manifest #477

Merged
merged 12 commits into from
Oct 13, 2024
12 changes: 6 additions & 6 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fbw-installer",
"productName": "FlyByWire Installer",
"version": "3.4.0-dev.1",
"version": "3.4.2-dev.1",
"description": "Desktop application to install and customize FlyByWire addons",
"configUrls": {
"production": "https://cdn.flybywiresim.com/installer/config/production.json",
Expand Down Expand Up @@ -103,7 +103,7 @@
"webpack-dev-server": "^4.4.0"
},
"dependencies": {
"@flybywiresim/fragmenter": "^0.7.4",
"@flybywiresim/fragmenter": "^0.8.0",
"@reduxjs/toolkit": "^1.7.1",
"@sentry/cli": "^2.31.0",
"@sentry/electron": "^4.23.0",
Expand Down
15 changes: 6 additions & 9 deletions src/renderer/components/AddonSection/Configure/TrackSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,25 @@ type TrackProps = {
};

export const Track: React.FC<TrackProps> = ({ isSelected, isInstalled, handleSelected, addon, track }) => {
const latestVersionName = useSelector<InstallerStore, string>(
(state) => state.latestVersionNames[addon.key]?.[track.key]?.name ?? '<unknown>',
const latestVersionName = useSelector<InstallerStore, string | undefined>(
(state) => state.latestVersionNames[addon.key]?.[track.key]?.name,
);

return (
<div
className={twMerge(
`relative flex w-60 cursor-pointer flex-row items-center rounded-sm-md border-2 border-transparent bg-navy-dark text-white transition-all duration-200 hover:border-navy-lightest hover:text-gray-300`,
`flex w-60 h-24 cursor-pointer flex-col rounded-sm-md border-2 border-transparent bg-navy-dark text-white transition-all duration-200 hover:border-navy-lightest hover:text-gray-300`,
isSelected && 'border-2 border-cyan text-cyan',
)}
onClick={() => handleSelected(track)}
>
<div
className={`h-12 w-1 rounded-r-xl transition-all duration-200${isSelected ? 'scale-y-100' : 'scale-y-50'}`}
/>
<div className="flex flex-col px-3 py-2.5">
<span className="text-xl text-current">{track.name}</span>
<span className="mt-0.5 font-manrope text-3xl font-medium tracking-wider text-current">
{latestVersionName}
<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>}
{isInstalled && <Check className={`-mt-3.5 stroke-current text-cyan`} strokeWidth={3} />}
</span>
</div>
{isInstalled && <Check className={`absolute right-4 stroke-current text-cyan`} strokeWidth={3} />}
</div>
);
};
12 changes: 6 additions & 6 deletions src/renderer/components/AddonSection/Configure/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import './index.css';
export interface ConfigureProps {
routeAspectKey: string;
selectedAddon: Addon;
selectedTrack: AddonTrack;
installedTrack: AddonTrack;
selectedTrack: AddonTrack | null;
installedTrack: AddonTrack | null;
onTrackSelection: (track: AddonTrack) => void;
}

Expand Down Expand Up @@ -40,8 +40,8 @@ export const Configure: FC<ConfigureProps> = ({
addon={selectedAddon}
key={track.key}
track={track}
isSelected={selectedTrack === track}
isInstalled={installedTrack === track}
isSelected={selectedTrack?.key === track.key}
isInstalled={installedTrack?.key === track.key}
handleSelected={() => onTrackSelection(track)}
/>
))}
Expand All @@ -57,8 +57,8 @@ export const Configure: FC<ConfigureProps> = ({
addon={selectedAddon}
key={track.key}
track={track}
isSelected={selectedTrack === track}
isInstalled={installedTrack === track}
isSelected={selectedTrack?.key === track.key}
isInstalled={installedTrack?.key === track.key}
handleSelected={() => onTrackSelection(track)}
/>
))}
Expand Down
95 changes: 11 additions & 84 deletions src/renderer/components/AddonSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@ import React, { FC, useCallback, useEffect, useState } from 'react';
import { setupInstallPath } from 'renderer/actions/install-path.utils';
import { DownloadItem } from 'renderer/redux/types';
import { useSelector } from 'react-redux';
import { getCurrentInstall } from '@flybywiresim/fragmenter';
import { InstallerStore, useAppDispatch, useAppSelector } from '../../redux/store';
import { Addon, AddonCategoryDefinition, AddonTrack } from 'renderer/utils/InstallerConfiguration';
import { Directories } from 'renderer/utils/Directories';
import { NavLink, Redirect, Route, useHistory, useParams } from 'react-router-dom';
import { Gear, InfoCircle, JournalText, Sliders } from 'react-bootstrap-icons';
import settings, { useSetting } from 'renderer/rendererSettings';
import { ipcRenderer } from 'electron';
import { AddonBar, AddonBarItem } from '../App/AddonBar';
import { NoAvailableAddonsSection } from '../NoAvailableAddonsSection';
import { ReleaseNotes } from './ReleaseNotes';
import { setInstalledTrack } from 'renderer/redux/features/installedTrack';
import { InstallState, setInstallStatus } from 'renderer/redux/features/installStatus';
import { setSelectedTrack } from 'renderer/redux/features/selectedTrack';
import { PromptModal, useModals } from 'renderer/components/Modal';
import ReactMarkdown from 'react-markdown';
Expand Down Expand Up @@ -128,13 +124,6 @@ export const AddonSection = (): JSX.Element => {

const installedTrack = (installedTracks[selectedAddon.key] as AddonTrack) ?? null;

const setCurrentlyInstalledTrack = useCallback(
(newInstalledTrack: AddonTrack) => {
dispatch(setInstalledTrack({ addonKey: selectedAddon.key, installedTrack: newInstalledTrack }));
},
[dispatch, selectedAddon.key],
);

const setCurrentlySelectedTrack = useCallback(
(newSelectedTrack: AddonTrack) => {
dispatch(setSelectedTrack({ addonKey: selectedAddon.key, track: newSelectedTrack }));
Expand All @@ -144,79 +133,12 @@ export const AddonSection = (): JSX.Element => {

const selectedTrack = (selectedTracks[selectedAddon.key] as AddonTrack) ?? null;

const selectAndSetTrack = useCallback(
(key: string) => {
const newTrack = selectedAddon.tracks.find((track) => track.key === key);
setCurrentlySelectedTrack(newTrack);
},
[selectedAddon.tracks, setCurrentlySelectedTrack],
);

const getCurrentInstallStatus = (): InstallState => {
try {
return installStates[selectedAddon.key];
} catch (e) {
setCurrentInstallStatus({ status: InstallStatus.Unknown });
return { status: InstallStatus.Unknown };
}
};

const setCurrentInstallStatus = useCallback(
(new_state: InstallState) => {
dispatch(setInstallStatus({ addonKey: selectedAddon.key, installState: new_state }));
},
[dispatch, selectedAddon.key],
);

const findInstalledTrack = useCallback((): AddonTrack => {
if (!Directories.isFragmenterInstall(selectedAddon)) {
console.log('Not installed');
if (selectedTrack) {
selectAndSetTrack(selectedTrack.key);
return selectedTrack;
} else {
setCurrentlySelectedTrack(selectedAddon.tracks[0]);
return selectedAddon.tracks[0];
}
}

try {
const manifest = getCurrentInstall(Directories.inInstallLocation(selectedAddon.targetDirectory));
console.log('Currently installed', manifest);

let track = selectedAddon.tracks.find((track) => track.url.includes(manifest.source));
if (!track) {
track = selectedAddon.tracks.find((track) => track.alternativeUrls?.includes(manifest.source));
}

console.log('Currently installed', track);
setCurrentlyInstalledTrack(track);
if (selectedTrack) {
selectAndSetTrack(selectedTrack.key);
return selectedTrack;
} else {
setCurrentlySelectedTrack(track);
return track;
}
} catch (e) {
console.error(e);
console.log('Not installed');
if (selectedTrack) {
selectAndSetTrack(selectedTrack.key);
return selectedTrack;
} else {
setCurrentlySelectedTrack(selectedAddon.tracks[0]);
return selectedAddon.tracks[0];
}
}
}, [selectAndSetTrack, selectedAddon, selectedTrack, setCurrentlyInstalledTrack, setCurrentlySelectedTrack]);

const download: DownloadItem = useSelector((state: InstallerStore) =>
state.downloads.find((download) => download.id === selectedAddon.key),
);

const isDownloading = download?.progress.totalPercent >= 0;
const status = getCurrentInstallStatus()?.status;
const status = installStates[selectedAddon.key]?.status;
const isInstalling = InstallStatusCategories.installing.includes(status);
const isFinishingDependencyInstall = status === InstallStatus.InstallingDependencyEnding;

Expand Down Expand Up @@ -254,11 +176,10 @@ export const AddonSection = (): JSX.Element => {
}, [dispatch, publisherData, selectedAddon]);

useEffect(() => {
findInstalledTrack();
if (!isInstalling) {
InstallManager.determineAddonInstallState(selectedAddon).then(setCurrentInstallStatus);
void InstallManager.refreshAddonInstallState(selectedAddon);
}
}, [findInstalledTrack, isInstalling, selectedAddon, setCurrentInstallStatus]);
}, [isInstalling, selectedAddon]);

useEffect(() => {
if (download && isDownloading) {
Expand Down Expand Up @@ -287,13 +208,19 @@ export const AddonSection = (): JSX.Element => {
bodyText={track.warningContent}
confirmColor={ButtonType.Caution}
onConfirm={() => {
selectAndSetTrack(track.key);
setCurrentlySelectedTrack(track);

// Update install state
void InstallManager.refreshAddonInstallState(selectedAddon);
}}
dontShowAgainSettingName="mainSettings.disableExperimentalWarning"
/>,
);
} else {
selectAndSetTrack(track.key);
setCurrentlySelectedTrack(track);

// Update install state
void InstallManager.refreshAddonInstallState(selectedAddon);
}
}
};
Expand Down
69 changes: 10 additions & 59 deletions src/renderer/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,21 @@ import SimpleBar from 'simplebar-react';
import { Logo } from 'renderer/components/Logo';
import { SettingsSection } from 'renderer/components/SettingsSection';
import { DebugSection } from 'renderer/components/DebugSection';
import { GitVersions } from '@flybywiresim/api-client';
import { DataCache } from '../../utils/DataCache';
import { InstallerUpdate } from 'renderer/components/InstallerUpdate';
import { WindowButtons } from 'renderer/components/WindowActionButtons';
import { Addon, AddonVersion } from 'renderer/utils/InstallerConfiguration';
import { AddonData } from 'renderer/utils/AddonData';
import { Addon } from 'renderer/utils/InstallerConfiguration';
import { ErrorModal } from '../ErrorModal';
import { NavBar, NavBarPublisher } from 'renderer/components/App/NavBar';
import { Redirect, Route, Switch, useHistory, useLocation } from 'react-router-dom';
import { store, useAppSelector } from 'renderer/redux/store';
import { setAddonAndTrackLatestReleaseInfo } from 'renderer/redux/features/latestVersionNames';
import { useAppSelector } from 'renderer/redux/store';
import settings from 'renderer/rendererSettings';
import './index.css';
import { ipcRenderer } from 'electron';
import channels from 'common/channels';
import { ModalContainer } from '../Modal';
import { PublisherSection } from 'renderer/components/PublisherSection';
import * as packageInfo from '../../../../package.json';

const releaseCache = new DataCache<AddonVersion[]>('releases', 1000 * 3600 * 24);

/**
* Obtain releases for a specific addon
*
* @param addon
*/
export const getAddonReleases = async (addon: Addon): Promise<AddonVersion[]> => {
const releases = (
await releaseCache.fetchOrCompute(async (): Promise<AddonVersion[]> => {
return (await GitVersions.getReleases(addon.repoOwner, addon.repoName))
.filter((r) => /v\d/.test(r.name))
.map((r) => ({ title: r.name, date: r.publishedAt, type: 'minor' }));
})
).map((r) => ({ ...r, date: new Date(r.date) })); // Local Data cache returns a string instead of Date

releases.forEach((version, index) => {
const currentVersionTitle = version.title;
const otherVersionTitle = index === releases.length - 1 ? releases[index - 1].title : releases[index + 1].title;

if (currentVersionTitle[1] !== otherVersionTitle[1]) {
releases[index].type = 'major';
} else if (currentVersionTitle[3] !== otherVersionTitle[3]) {
releases[index].type = 'minor';
} else if (currentVersionTitle[5] !== otherVersionTitle[5] && index === releases.length - 1) {
releases[index].type = 'minor';
} else if (currentVersionTitle[5] !== otherVersionTitle[5]) {
releases[index].type = 'patch';
}
});

return releases;
};

export const fetchLatestVersionNames = async (addon: Addon): Promise<void> => {
const dispatch = store.dispatch;

for (const track of addon.tracks) {
const trackLatestVersionName = await AddonData.latestVersionForTrack(addon, track);
dispatch(
setAddonAndTrackLatestReleaseInfo({
addonKey: addon.key,
trackKey: track.key,
info: trackLatestVersionName,
}),
);
}
};
import { InstallManager } from 'renderer/utils/InstallManager';

const App = () => {
const history = useHistory();
Expand All @@ -86,8 +34,9 @@ const App = () => {
);

useEffect(() => {
addons.forEach(AddonData.configureInitialAddonState);
addons.forEach(fetchLatestVersionNames);
for (const addon of addons) {
void InstallManager.refreshAddonInstallState(addon).then(() => void InstallManager.checkForUpdates(addon));
}

if (settings.get('cache.main.lastShownSection')) {
history.push(settings.get('cache.main.lastShownSection'));
Expand All @@ -103,8 +52,10 @@ const App = () => {
const updateCheck = setInterval(
() => {
ipcRenderer.send(channels.checkForInstallerUpdate);
addons.forEach(AddonData.checkForUpdates);
addons.forEach(fetchLatestVersionNames);

for (const addon of addons) {
void InstallManager.checkForUpdates(addon);
}
},
5 * 60 * 1000,
);
Expand Down
Loading
Loading