mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-06 09:30:29 +00:00
feat(release): add cross-platform packaging
Add target-aware engine provisioning, platform package configs, and CI/release verification for macOS arm64, Windows x64, and Linux AppImage.
This commit is contained in:
@@ -13,8 +13,10 @@ import { canonicalizeDownloadFileName, categoryForFileName } from '../utils/down
|
||||
import { fetchMediaMetadataDeduped } from '../utils/mediaMetadata';
|
||||
import {
|
||||
resolveCategoryDestination,
|
||||
resolveDownloadFilePath
|
||||
resolveDownloadFilePath,
|
||||
downloadLocationEquals
|
||||
} from '../utils/downloadLocations';
|
||||
import { getPlatformInfo } from '../utils/platform';
|
||||
import { isTransferLocked } from '../utils/downloadActions';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import {
|
||||
@@ -319,9 +321,10 @@ export const AddDownloadsModal = () => {
|
||||
directory: true,
|
||||
multiple: false,
|
||||
defaultPath: saveLocation.startsWith('~') ? undefined : saveLocation
|
||||
});
|
||||
});
|
||||
if (selected && typeof selected === 'string') {
|
||||
setSaveLocation(selected);
|
||||
const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected);
|
||||
setSaveLocation(approvedPath);
|
||||
setIsSaveLocationManual(true);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -347,6 +350,7 @@ export const AddDownloadsModal = () => {
|
||||
let useSharedDestination = isSaveLocationManual;
|
||||
const destinationOverrides: Record<number, string> = {};
|
||||
const settings = useSettingsStore.getState();
|
||||
const platform = await getPlatformInfo().catch(() => ({ os: 'unknown' }));
|
||||
if (settings.askWhereToSaveEachFile && parsedItems.length > 0) {
|
||||
for (const [index, item] of parsedItems.entries()) {
|
||||
try {
|
||||
@@ -360,7 +364,8 @@ export const AddDownloadsModal = () => {
|
||||
defaultPath: suggestedLocation.startsWith('~') ? undefined : suggestedLocation
|
||||
});
|
||||
if (selected && typeof selected === 'string') {
|
||||
destinationOverrides[index] = selected;
|
||||
const approvedPath = await useSettingsStore.getState().approveDownloadRoot(selected);
|
||||
destinationOverrides[index] = approvedPath;
|
||||
} else {
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
@@ -398,8 +403,13 @@ export const AddDownloadsModal = () => {
|
||||
const destination = download.destination ||
|
||||
await resolveCategoryDestination(settings, download.category);
|
||||
if (
|
||||
destination === itemLocation &&
|
||||
download.fileName === finalFile &&
|
||||
downloadLocationEquals(
|
||||
destination,
|
||||
download.fileName,
|
||||
itemLocation,
|
||||
finalFile,
|
||||
platform.os
|
||||
) &&
|
||||
download.status !== 'failed'
|
||||
) {
|
||||
fileExistsInStore = true;
|
||||
@@ -456,6 +466,7 @@ export const AddDownloadsModal = () => {
|
||||
destinationOverrides: Record<number, string> = {}
|
||||
) => {
|
||||
let itemsToAdd: Array<AddDownloadDraftRow | null> = [...parsedItems];
|
||||
const platform = await getPlatformInfo().catch(() => ({ os: 'unknown' }));
|
||||
|
||||
if (resolutions) {
|
||||
for (const res of resolutions) {
|
||||
@@ -491,8 +502,13 @@ export const AddDownloadsModal = () => {
|
||||
const destination = download.destination ||
|
||||
await resolveCategoryDestination(currentSettings, download.category);
|
||||
if (
|
||||
destination === itemLocation &&
|
||||
download.fileName === newName &&
|
||||
downloadLocationEquals(
|
||||
destination,
|
||||
download.fileName,
|
||||
itemLocation,
|
||||
newName,
|
||||
platform.os
|
||||
) &&
|
||||
download.status !== 'failed'
|
||||
) {
|
||||
storeHas = true;
|
||||
@@ -534,8 +550,13 @@ export const AddDownloadsModal = () => {
|
||||
const destination = download.destination ||
|
||||
await resolveCategoryDestination(currentSettings, download.category);
|
||||
if (
|
||||
destination === itemLocation &&
|
||||
download.fileName === finalFile &&
|
||||
downloadLocationEquals(
|
||||
destination,
|
||||
download.fileName,
|
||||
itemLocation,
|
||||
finalFile,
|
||||
platform.os
|
||||
) &&
|
||||
download.status !== 'failed'
|
||||
) {
|
||||
existingItem = download;
|
||||
|
||||
@@ -211,8 +211,8 @@ export default function LogsView() {
|
||||
<div className="bg-black/10 border-y border-border-modal px-4 py-2 shrink-0 flex items-center gap-2 text-text-muted text-[10px] select-none">
|
||||
<Info size={12} className="text-text-muted opacity-80 shrink-0" />
|
||||
<span className="opacity-90 leading-tight">
|
||||
<strong className="font-medium text-text-primary mr-1">Privacy Note:</strong>
|
||||
Telemetry securely captures basic hardware capabilities (OS, CPU, RAM) exclusively for troubleshooting. No unique identifiers or sensitive paths are collected. Logs remain entirely offline on your device until manually exported.
|
||||
<strong className="font-medium text-text-primary mr-1">Local diagnostics:</strong>
|
||||
Firelink keeps bounded rotating logs on this device for troubleshooting. Common secrets, URL queries, and home-directory paths are redacted during display and export. Nothing is uploaded automatically.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PostQueueAction, SchedulerSettings, useSettingsStore } from '../store/u
|
||||
import { MAIN_QUEUE_ID, useDownloadStore } from '../store/useDownloadStore';
|
||||
import { WindowDragRegion } from './WindowDragRegion';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { usePlatformInfo } from '../utils/platform';
|
||||
|
||||
const days = [
|
||||
{ value: 0, label: 'Su' },
|
||||
@@ -65,7 +66,8 @@ export default function SchedulerView() {
|
||||
const { addToast } = useToast();
|
||||
const [permissionMessage, setPermissionMessage] = useState('');
|
||||
const [automationPermissionGranted, setAutomationPermissionGranted] = useState<boolean | null>(null);
|
||||
const isMac = navigator.userAgent.includes('Mac');
|
||||
const platform = usePlatformInfo();
|
||||
const isMac = platform.os === 'macos';
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(savedSettings);
|
||||
@@ -365,7 +367,7 @@ export default function SchedulerView() {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{isMac && (
|
||||
{isMac ? (
|
||||
<section className="app-card mt-4 max-w-[760px] p-5">
|
||||
<div className="mb-2 flex items-center gap-2 font-semibold text-text-primary">
|
||||
<LockKeyhole size={17} className="text-accent" /> System Permissions
|
||||
@@ -393,6 +395,16 @@ export default function SchedulerView() {
|
||||
</div>
|
||||
{permissionMessage && <p className="mt-3 text-[11px] text-text-muted">{permissionMessage}</p>}
|
||||
</section>
|
||||
) : (
|
||||
<section className="app-card mt-4 max-w-[760px] p-5">
|
||||
<div className="mb-2 flex items-center gap-2 font-semibold text-text-primary">
|
||||
<LockKeyhole size={17} className="text-accent" /> System Actions
|
||||
</div>
|
||||
<p className="text-[12px] text-text-muted">
|
||||
Sleep, restart, and shut down use {platform.os === 'windows' ? 'Windows system privileges' : 'your Linux desktop and system policy'}.
|
||||
Firelink reports any rejected action when it runs; no permanent permission is claimed in advance.
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
DOWNLOAD_CATEGORIES,
|
||||
normalizeCategorySubfolder
|
||||
} from '../utils/downloadLocations';
|
||||
import { usePlatformInfo } from '../utils/platform';
|
||||
|
||||
const settingsTabs: { type: SettingsTab; label: string; icon: typeof Download }[] = [
|
||||
{ type: 'downloads', label: 'Downloads', icon: Download },
|
||||
@@ -176,6 +177,7 @@ const CategoryFolderInput = ({
|
||||
export default function SettingsView() {
|
||||
const settings = useSettingsStore();
|
||||
const activeTab = settings.activeSettingsTab;
|
||||
const platform = usePlatformInfo();
|
||||
|
||||
// Local state for engine status
|
||||
const [engineStatus, setEngineStatus] = useState<EngineStatusItem[] | null>(null);
|
||||
@@ -362,7 +364,8 @@ runEngineChecks(false);
|
||||
defaultPath: currentPath.startsWith('~') ? undefined : currentPath
|
||||
});
|
||||
if (selected && typeof selected === 'string') {
|
||||
settings.setCategoryDirectoryOverride(category, selected);
|
||||
const approvedPath = await settings.approveDownloadRoot(selected);
|
||||
settings.setCategoryDirectoryOverride(category, approvedPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to select folder for ${category}:`, e);
|
||||
@@ -379,7 +382,8 @@ runEngineChecks(false);
|
||||
: settings.baseDownloadFolder
|
||||
});
|
||||
if (base && typeof base === 'string') {
|
||||
settings.setBaseDownloadFolder(base);
|
||||
const approvedBase = await settings.approveDownloadRoot(base);
|
||||
settings.setBaseDownloadFolder(approvedBase);
|
||||
try {
|
||||
const safeSubfolders = Object.fromEntries(
|
||||
DOWNLOAD_CATEGORIES.map(category => [
|
||||
@@ -391,7 +395,7 @@ runEngineChecks(false);
|
||||
])
|
||||
);
|
||||
await invoke('create_category_directories', {
|
||||
baseFolder: base,
|
||||
baseFolder: approvedBase,
|
||||
subfolders: safeSubfolders
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -647,24 +651,28 @@ runEngineChecks(false);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="settings-section-title">macOS Integration</h2>
|
||||
<h2 className="settings-section-title">
|
||||
{platform.os === 'macos' ? 'macOS Integration' : 'Desktop Integration'}
|
||||
</h2>
|
||||
<div className="mac-settings-group">
|
||||
{platform.os === 'macos' && (
|
||||
<label className="mac-settings-row cursor-default">
|
||||
<div className="settings-row-label">
|
||||
<span>Show badge on Dock icon</span>
|
||||
<small>Displays active download count on Firelink Dock icon.</small>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showDockBadge}
|
||||
onChange={(e) => settings.setShowDockBadge(e.target.checked)}
|
||||
className="mac-switch"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<label className="mac-settings-row cursor-default">
|
||||
<div className="settings-row-label">
|
||||
<span>Show badge on Dock icon</span>
|
||||
<small>Displays the number of active downloads on the Firelink Dock icon.</small>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showDockBadge}
|
||||
onChange={(e) => settings.setShowDockBadge(e.target.checked)}
|
||||
className="mac-switch"
|
||||
/>
|
||||
</label>
|
||||
<label className="mac-settings-row cursor-default">
|
||||
<div className="settings-row-label">
|
||||
<span>Show menu bar icon</span>
|
||||
<small>Provides quick access to downloads and queues from the macOS menu bar.</small>
|
||||
<span>{platform.os === 'macos' ? 'Show menu bar icon' : 'Show system tray icon'}</span>
|
||||
<small>Provides quick access to downloads and queues.</small>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -732,7 +740,7 @@ runEngineChecks(false);
|
||||
)}
|
||||
<p className="settings-group-footer">
|
||||
{settings.proxyMode === 'none' && 'Downloads ignore configured proxies.'}
|
||||
{settings.proxyMode === 'system' && 'Downloads use the matching macOS system proxy when one is configured.'}
|
||||
{settings.proxyMode === 'system' && `Downloads use the detected ${platform.os === 'macos' ? 'macOS' : platform.os === 'windows' ? 'Windows' : 'desktop'} system proxy when available.`}
|
||||
{settings.proxyMode === 'custom' && (settings.proxyHost
|
||||
? `Downloads use http://${settings.proxyHost}:${settings.proxyPort}.`
|
||||
: 'Enter a proxy host and port to enable the custom proxy.')}
|
||||
|
||||
Reference in New Issue
Block a user