mirror of
https://github.com/abusoww/tuxmate.git
synced 2026-04-17 15:53:24 +02:00
feat(nix): show unfree packages warning in UI ref #35
This commit is contained in:
@@ -38,7 +38,9 @@ export default function Home() {
|
||||
aurAppNames,
|
||||
isHydrated,
|
||||
selectedHelper,
|
||||
setSelectedHelper
|
||||
setSelectedHelper,
|
||||
hasUnfreePackages,
|
||||
unfreeAppNames,
|
||||
} = useLinuxInit();
|
||||
|
||||
// Search state
|
||||
@@ -349,6 +351,8 @@ export default function Home() {
|
||||
clearAll={clearAll}
|
||||
selectedHelper={selectedHelper}
|
||||
setSelectedHelper={setSelectedHelper}
|
||||
hasUnfreePackages={hasUnfreePackages}
|
||||
unfreeAppNames={unfreeAppNames}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { Check, Copy, X, Download } from 'lucide-react';
|
||||
import { Check, Copy, X, Download, AlertTriangle } from 'lucide-react';
|
||||
import { AurDrawerSettings } from './AurDrawerSettings';
|
||||
import type { DistroId } from '@/lib/data';
|
||||
|
||||
interface CommandDrawerProps {
|
||||
isOpen: boolean;
|
||||
@@ -21,13 +22,14 @@ interface CommandDrawerProps {
|
||||
selectedHelper: 'yay' | 'paru';
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
distroColor: string;
|
||||
distroId: DistroId;
|
||||
// Nix unfree warning
|
||||
hasUnfreePackages?: boolean;
|
||||
unfreeAppNames?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Command drawer that shows the generated install command.
|
||||
* Acts as a bottom sheet on mobile (swipe to dismiss) and a centered modal on desktop.
|
||||
* If you're reading this, yes, I did spend way too much time on the animations.
|
||||
*/
|
||||
// Command drawer - bottom sheet on mobile, modal on desktop.
|
||||
// Nix gets special treatment: shows config file instead of terminal command.
|
||||
export function CommandDrawer({
|
||||
isOpen,
|
||||
isClosing,
|
||||
@@ -44,7 +46,11 @@ export function CommandDrawer({
|
||||
selectedHelper,
|
||||
setSelectedHelper,
|
||||
distroColor,
|
||||
distroId,
|
||||
hasUnfreePackages = false,
|
||||
unfreeAppNames = [],
|
||||
}: CommandDrawerProps) {
|
||||
const isNix = distroId === 'nix';
|
||||
// Swipe-to-dismiss for mobile users who hate tapping tiny X buttons
|
||||
const [dragOffset, setDragOffset] = useState(0);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
@@ -126,7 +132,9 @@ export function CommandDrawer({
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-5 rounded-full" style={{ backgroundColor: distroColor }}></div>
|
||||
<div>
|
||||
<h3 id="drawer-title" className="text-sm font-bold uppercase tracking-wider text-[var(--text-secondary)]">Terminal Preview</h3>
|
||||
<h3 id="drawer-title" className="text-sm font-bold uppercase tracking-wider text-[var(--text-secondary)]">
|
||||
{isNix ? 'Configuration Preview' : 'Terminal Preview'}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--text-muted)] mt-0.5">
|
||||
{selectedCount} apps selected
|
||||
</p>
|
||||
@@ -154,10 +162,25 @@ export function CommandDrawer({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Nix unfree packages warning */}
|
||||
{isNix && hasUnfreePackages && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-500 mt-0.5 shrink-0" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-amber-500">Unfree packages</p>
|
||||
<p className="text-[var(--text-muted)] mt-1">
|
||||
{unfreeAppNames.join(', ')} require <code className="px-1 py-0.5 rounded bg-[var(--bg-tertiary)] text-xs">nixpkgs.config.allowUnfree = true</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Terminal preview - where the magic gets displayed */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-primary)] overflow-hidden shadow-sm">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-[var(--bg-tertiary)] border-b border-[var(--border-primary)]">
|
||||
<span className="text-xs font-mono text-[var(--text-muted)]">bash</span>
|
||||
<span className="text-xs font-mono text-[var(--text-muted)]">{isNix ? 'nix' : 'bash'}</span>
|
||||
|
||||
{/* Desktop action buttons */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
@@ -166,7 +189,7 @@ export function CommandDrawer({
|
||||
className="h-8 px-4 flex items-center gap-2 rounded-md hover:bg-[var(--bg-primary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all text-xs font-medium"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Script</span>
|
||||
<span>{isNix ? 'configuration.nix' : 'Script'}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCopyAndClose}
|
||||
@@ -187,7 +210,7 @@ export function CommandDrawer({
|
||||
|
||||
<div className="p-4 font-mono text-sm overflow-x-auto bg-[var(--bg-secondary)]">
|
||||
<div className="flex gap-3">
|
||||
<span className="select-none shrink-0 font-bold" style={{ color: distroColor }}>$</span>
|
||||
{!isNix && <span className="select-none shrink-0 font-bold" style={{ color: distroColor }}>$</span>}
|
||||
<code
|
||||
className="text-[var(--text-primary)] break-all whitespace-pre-wrap select-text"
|
||||
style={{
|
||||
|
||||
@@ -25,13 +25,12 @@ interface CommandFooterProps {
|
||||
clearAll: () => void;
|
||||
selectedHelper: 'yay' | 'paru';
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
// Nix unfree
|
||||
hasUnfreePackages?: boolean;
|
||||
unfreeAppNames?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The sticky footer that shows the generated command and action buttons.
|
||||
* Contains more state than I'd like, but hey, it works.
|
||||
* Keyboard shortcuts are vim-style because we're not savages.
|
||||
*/
|
||||
|
||||
export function CommandFooter({
|
||||
command,
|
||||
selectedCount,
|
||||
@@ -47,6 +46,8 @@ export function CommandFooter({
|
||||
clearAll,
|
||||
selectedHelper,
|
||||
setSelectedHelper,
|
||||
hasUnfreePackages,
|
||||
unfreeAppNames,
|
||||
}: CommandFooterProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
@@ -56,7 +57,7 @@ export function CommandFooter({
|
||||
|
||||
const { toggle: toggleTheme } = useTheme();
|
||||
|
||||
// Track if user has actually interacted - we hide the bar until then.
|
||||
// Track if user has actually interacted - hide the bar until then.
|
||||
// Otherwise it just sits there looking sad with "No apps selected".
|
||||
useEffect(() => {
|
||||
if (selectedCount !== initialCountRef.current && !hasEverHadSelection) {
|
||||
@@ -103,11 +104,14 @@ export function CommandFooter({
|
||||
selectedAppIds: selectedApps,
|
||||
helper: selectedHelper,
|
||||
});
|
||||
const blob = new Blob([script], { type: 'text/x-shellscript' });
|
||||
const isNix = selectedDistro === 'nix';
|
||||
const ext = isNix ? 'nix' : 'sh';
|
||||
const mimeType = isNix ? 'text/plain' : 'text/x-shellscript';
|
||||
const blob = new Blob([script], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `tuxmate-${selectedDistro}.sh`;
|
||||
a.download = isNix ? 'configuration.nix' : `tuxmate-${selectedDistro}.sh`;
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
const distroName = distros.find(d => d.id === selectedDistro)?.name || selectedDistro;
|
||||
@@ -182,6 +186,9 @@ export function CommandFooter({
|
||||
selectedHelper={selectedHelper}
|
||||
setSelectedHelper={setSelectedHelper}
|
||||
distroColor={distroColor}
|
||||
distroId={selectedDistro}
|
||||
hasUnfreePackages={hasUnfreePackages}
|
||||
unfreeAppNames={unfreeAppNames}
|
||||
/>
|
||||
|
||||
{/* Animated footer container - only shows after first selection */}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { distros, apps, type DistroId } from '@/lib/data';
|
||||
import { isAurPackage } from '@/lib/aur';
|
||||
import { isUnfreePackage } from '@/lib/nixUnfree';
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export { isAurPackage, AUR_PATTERNS, KNOWN_AUR_PACKAGES } from '@/lib/aur';
|
||||
@@ -29,6 +30,9 @@ export interface UseLinuxInitReturn {
|
||||
hasAurPackages: boolean;
|
||||
aurPackageNames: string[];
|
||||
aurAppNames: string[];
|
||||
// Nix unfree specific
|
||||
hasUnfreePackages: boolean;
|
||||
unfreeAppNames: string[];
|
||||
// Hydration state
|
||||
isHydrated: boolean;
|
||||
}
|
||||
@@ -118,6 +122,26 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
return { hasAur: aurPkgs.length > 0, packages: aurPkgs, appNames: aurAppNames };
|
||||
}, [selectedDistro, selectedApps]);
|
||||
|
||||
// Compute unfree package info for Nix
|
||||
const unfreePackageInfo = useMemo(() => {
|
||||
if (selectedDistro !== 'nix') {
|
||||
return { hasUnfree: false, appNames: [] as string[] };
|
||||
}
|
||||
|
||||
const unfreeAppNames: string[] = [];
|
||||
selectedApps.forEach(appId => {
|
||||
const app = apps.find(a => a.id === appId);
|
||||
if (app) {
|
||||
const pkg = app.targets['nix'];
|
||||
if (pkg && isUnfreePackage(pkg)) {
|
||||
unfreeAppNames.push(app.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { hasUnfree: unfreeAppNames.length > 0, appNames: unfreeAppNames };
|
||||
}, [selectedDistro, selectedApps]);
|
||||
|
||||
const isAppAvailable = useCallback((appId: string): boolean => {
|
||||
const app = apps.find(a => a.id === appId);
|
||||
if (!app) return false;
|
||||
@@ -206,11 +230,11 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
|
||||
if (packageNames.length === 0) return '# No packages selected';
|
||||
|
||||
// Handle special cases for Nix and Snap
|
||||
// Nix: show declarative config (no unfree warning in preview - that's in download)
|
||||
if (selectedDistro === 'nix') {
|
||||
// installPrefix already ends with 'nixpkgs.' so just join packages with ' nixpkgs.'
|
||||
const filteredPkgs = packageNames.filter(p => p.trim());
|
||||
return `${distro.installPrefix}${filteredPkgs.join(' nixpkgs.')}`;
|
||||
const sortedPkgs = packageNames.filter(p => p.trim()).sort();
|
||||
const pkgList = sortedPkgs.map(p => ` ${p}`).join('\n');
|
||||
return `environment.systemPackages = with pkgs; [\n${pkgList}\n];`;
|
||||
}
|
||||
|
||||
if (selectedDistro === 'snap') {
|
||||
@@ -280,6 +304,9 @@ export function useLinuxInit(): UseLinuxInitReturn {
|
||||
hasAurPackages: aurPackageInfo.hasAur,
|
||||
aurPackageNames: aurPackageInfo.packages,
|
||||
aurAppNames: aurPackageInfo.appNames,
|
||||
// Nix unfree specific
|
||||
hasUnfreePackages: unfreePackageInfo.hasUnfree,
|
||||
unfreeAppNames: unfreePackageInfo.appNames,
|
||||
// Hydration state
|
||||
isHydrated: hydrated,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user