License and updater Cargo features (#258)

This commit is contained in:
Gregory Schier
2025-09-29 22:08:05 -07:00
committed by GitHub
parent 6c79c1ef3f
commit 757d28c235
11 changed files with 124 additions and 52 deletions

View File

@@ -114,4 +114,4 @@ jobs:
releaseBody: '[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)' releaseBody: '[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)'
releaseDraft: true releaseDraft: true
prerelease: false prerelease: false
args: ${{ matrix.args }} args: '${{ matrix.args }} --features "updater license"'

View File

@@ -32,6 +32,9 @@ strip = true # Automatically strip symbols from the binary.
[features] [features]
cargo-clippy = [] cargo-clippy = []
default = []
updater = []
license = ["yaak-license"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.4.1", features = [] } tauri-build = { version = "2.4.1", features = [] }
@@ -74,7 +77,7 @@ yaak-fonts = { workspace = true }
yaak-git = { path = "yaak-git" } yaak-git = { path = "yaak-git" }
yaak-grpc = { path = "yaak-grpc" } yaak-grpc = { path = "yaak-grpc" }
yaak-http = { workspace = true } yaak-http = { workspace = true }
yaak-license = { path = "yaak-license" } yaak-license = { path = "yaak-license", optional = true }
yaak-mac-window = { path = "yaak-mac-window" } yaak-mac-window = { path = "yaak-mac-window" }
yaak-models = { workspace = true } yaak-models = { workspace = true }
yaak-plugins = { workspace = true } yaak-plugins = { workspace = true }

View File

@@ -73,6 +73,8 @@ struct AppMetaData {
name: String, name: String,
app_data_dir: String, app_data_dir: String,
app_log_dir: String, app_log_dir: String,
feature_updater: bool,
feature_license: bool,
} }
#[tauri::command] #[tauri::command]
@@ -85,6 +87,8 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult<AppMetaData> {
name: app_handle.package_info().name.to_string(), name: app_handle.package_info().name.to_string(),
app_data_dir: app_data_dir.to_string_lossy().to_string(), app_data_dir: app_data_dir.to_string_lossy().to_string(),
app_log_dir: app_log_dir.to_string_lossy().to_string(), app_log_dir: app_log_dir.to_string_lossy().to_string(),
feature_license: cfg!(feature = "license"),
feature_updater: cfg!(feature = "updater"),
}) })
} }
@@ -1254,7 +1258,6 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(yaak_license::init())
.plugin(yaak_mac_window::init()) .plugin(yaak_mac_window::init())
.plugin(yaak_models::init()) .plugin(yaak_models::init())
.plugin(yaak_plugins::init()) .plugin(yaak_plugins::init())
@@ -1264,6 +1267,11 @@ pub fn run() {
.plugin(yaak_ws::init()) .plugin(yaak_ws::init())
.plugin(yaak_sync::init()); .plugin(yaak_sync::init());
#[cfg(feature = "license")]
{
builder = builder.plugin(yaak_license::init());
}
builder builder
.setup(|app| { .setup(|app| {
{ {
@@ -1380,19 +1388,21 @@ pub fn run() {
label, label,
.. ..
} => { } => {
let w = app_handle.get_webview_window(&label).unwrap(); if cfg!(feature = "updater") {
let h = app_handle.clone(); // Run update check whenever the window is focused
let w = app_handle.get_webview_window(&label).unwrap();
// Run update check whenever the window is focused let h = app_handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
if w.db().get_settings().autoupdate { if w.db().get_settings().autoupdate {
let val: State<'_, Mutex<YaakUpdater>> = h.state(); let val: State<'_, Mutex<YaakUpdater>> = h.state();
let update_mode = get_update_mode(&w).await.unwrap(); let update_mode = get_update_mode(&w).await.unwrap();
if let Err(e) = val.lock().await.maybe_check(&w, update_mode).await { if let Err(e) = val.lock().await.maybe_check(&w, update_mode).await
warn!("Failed to check for updates {e:?}"); {
warn!("Failed to check for updates {e:?}");
}
}; };
}; });
}); }
let h = app_handle.clone(); let h = app_handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {

View File

@@ -0,0 +1,20 @@
import type { ReactNode } from 'react';
import { appInfo } from '../lib/appInfo';
interface Props {
children: ReactNode;
feature: 'updater' | 'license';
}
const featureMap: Record<Props['feature'], boolean> = {
updater: appInfo.featureUpdater,
license: appInfo.featureLicense,
};
export function CargoFeature({ children, feature }: Props) {
if (featureMap[feature]) {
return <>{children}</>;
} else {
return null;
}
}

View File

@@ -5,6 +5,7 @@ import { useAtomValue } from 'jotai';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { openSettings } from '../commands/openSettings'; import { openSettings } from '../commands/openSettings';
import { appInfo } from '../lib/appInfo'; import { appInfo } from '../lib/appInfo';
import { CargoFeature } from './CargoFeature';
import { BadgeButton } from './core/BadgeButton'; import { BadgeButton } from './core/BadgeButton';
import type { ButtonProps } from './core/Button'; import type { ButtonProps } from './core/Button';
@@ -19,6 +20,14 @@ const details: Record<
}; };
export function LicenseBadge() { export function LicenseBadge() {
return (
<CargoFeature feature="license">
<LicenseBadgeCmp />
</CargoFeature>
);
}
function LicenseBadgeCmp() {
const { check } = useLicense(); const { check } = useLicense();
const settings = useAtomValue(settingsAtom); const settings = useAtomValue(settingsAtom);

View File

@@ -4,8 +4,10 @@ import { type } from '@tauri-apps/plugin-os';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useKeyPressEvent } from 'react-use'; import { useKeyPressEvent } from 'react-use';
import { appInfo } from '../../lib/appInfo';
import { capitalize } from '../../lib/capitalize'; import { capitalize } from '../../lib/capitalize';
import { HStack } from '../core/Stacks'; import { HStack } from '../core/Stacks';
import type { TabItem } from '../core/Tabs/Tabs';
import { TabContent, Tabs } from '../core/Tabs/Tabs'; import { TabContent, Tabs } from '../core/Tabs/Tabs';
import { HeaderSize } from '../HeaderSize'; import { HeaderSize } from '../HeaderSize';
import { SettingsInterface } from './SettingsInterface'; import { SettingsInterface } from './SettingsInterface';
@@ -72,21 +74,27 @@ export default function Settings({ hide }: Props) {
tabListClassName="min-w-[10rem] bg-surface x-theme-sidebar border-r border-border pl-3" tabListClassName="min-w-[10rem] bg-surface x-theme-sidebar border-r border-border pl-3"
label="Settings" label="Settings"
onChangeValue={setTab} onChangeValue={setTab}
tabs={tabs.map((value) => ({ value, label: capitalize(value) }))} tabs={tabs.map(
(value): TabItem => ({
value,
label: capitalize(value),
hidden: !appInfo.featureLicense && value === TAB_LICENSE,
}),
)}
> >
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full p-8"> <TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-8 !py-4">
<SettingsGeneral /> <SettingsGeneral />
</TabContent> </TabContent>
<TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full p-8"> <TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full px-8 !py-4">
<SettingsInterface /> <SettingsInterface />
</TabContent> </TabContent>
<TabContent value={TAB_THEME} className="overflow-y-auto h-full p-8"> <TabContent value={TAB_THEME} className="overflow-y-auto h-full px-8 !py-4">
<SettingsTheme /> <SettingsTheme />
</TabContent> </TabContent>
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 p-8"> <TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 px-8 !py-4">
<SettingsPlugins /> <SettingsPlugins />
</TabContent> </TabContent>
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full p-8!"> <TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-8 !py-4">
<SettingsProxy /> <SettingsProxy />
</TabContent> </TabContent>
<TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-8 !py-4"> <TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-8 !py-4">

View File

@@ -6,6 +6,7 @@ import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { appInfo } from '../../lib/appInfo'; import { appInfo } from '../../lib/appInfo';
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates'; import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
import { revealInFinderText } from '../../lib/reveal'; import { revealInFinderText } from '../../lib/reveal';
import { CargoFeature } from '../CargoFeature';
import { Checkbox } from '../core/Checkbox'; import { Checkbox } from '../core/Checkbox';
import { Heading } from '../core/Heading'; import { Heading } from '../core/Heading';
import { IconButton } from '../core/IconButton'; import { IconButton } from '../core/IconButton';
@@ -26,43 +27,45 @@ export function SettingsGeneral() {
return ( return (
<VStack space={1.5} className="mb-4"> <VStack space={1.5} className="mb-4">
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1"> <CargoFeature feature="updater">
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
<Select
name="updateChannel"
label="Update Channel"
labelPosition="left"
labelClassName="w-[14rem]"
size="sm"
value={settings.updateChannel}
onChange={(updateChannel) => patchModel(settings, { updateChannel })}
options={[
{ label: 'Stable', value: 'stable' },
{ label: 'Beta (more frequent)', value: 'beta' },
]}
/>
<IconButton
variant="border"
size="sm"
title="Check for updates"
icon="refresh"
spin={checkForUpdates.isPending}
onClick={() => checkForUpdates.mutateAsync()}
/>
</div>
<Select <Select
name="updateChannel" name="autoupdate"
label="Update Channel" value={settings.autoupdate ? 'auto' : 'manual'}
label="Update Behavior"
labelPosition="left" labelPosition="left"
labelClassName="w-[14rem]"
size="sm" size="sm"
value={settings.updateChannel} labelClassName="w-[14rem]"
onChange={(updateChannel) => patchModel(settings, { updateChannel })} onChange={(v) => patchModel(settings, { autoupdate: v === 'auto' })}
options={[ options={[
{ label: 'Stable', value: 'stable' }, { label: 'Automatic', value: 'auto' },
{ label: 'Beta (more frequent)', value: 'beta' }, { label: 'Manual', value: 'manual' },
]} ]}
/> />
<IconButton </CargoFeature>
variant="border"
size="sm"
title="Check for updates"
icon="refresh"
spin={checkForUpdates.isPending}
onClick={() => checkForUpdates.mutateAsync()}
/>
</div>
<Select
name="autoupdate"
value={settings.autoupdate ? 'auto' : 'manual'}
label="Update Behavior"
labelPosition="left"
size="sm"
labelClassName="w-[14rem]"
onChange={(v) => patchModel(settings, { autoupdate: v === 'auto' })}
options={[
{ label: 'Automatic', value: 'auto' },
{ label: 'Manual', value: 'manual' },
]}
/>
<Select <Select
name="switchWorkspaceBehavior" name="switchWorkspaceBehavior"

View File

@@ -4,6 +4,7 @@ import { differenceInDays } from 'date-fns';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useToggle } from '../../hooks/useToggle'; import { useToggle } from '../../hooks/useToggle';
import { pluralizeCount } from '../../lib/pluralize'; import { pluralizeCount } from '../../lib/pluralize';
import { CargoFeature } from '../CargoFeature';
import { Banner } from '../core/Banner'; import { Banner } from '../core/Banner';
import { Button } from '../core/Button'; import { Button } from '../core/Button';
import { Icon } from '../core/Icon'; import { Icon } from '../core/Icon';
@@ -13,6 +14,14 @@ import { HStack, VStack } from '../core/Stacks';
import { LocalImage } from '../LocalImage'; import { LocalImage } from '../LocalImage';
export function SettingsLicense() { export function SettingsLicense() {
return (
<CargoFeature feature="license">
<SettingsLicenseCmp />
</CargoFeature>
);
}
function SettingsLicenseCmp() {
const { check, activate, deactivate } = useLicense(); const { check, activate, deactivate } = useLicense();
const [key, setKey] = useState<string>(''); const [key, setKey] = useState<string>('');
const [activateFormVisible, toggleActivateFormVisible] = useToggle(false); const [activateFormVisible, toggleActivateFormVisible] = useToggle(false);

View File

@@ -74,6 +74,7 @@ export function SettingsDropdown() {
{ {
label: 'Check for Updates', label: 'Check for Updates',
leftSlot: <Icon icon="update" />, leftSlot: <Icon icon="update" />,
hidden: !appInfo.featureUpdater,
onSelect: () => checkForUpdates.mutate(), onSelect: () => checkForUpdates.mutate(),
}, },
{ {

View File

@@ -10,6 +10,7 @@ export type TabItem =
| { | {
value: string; value: string;
label: string; label: string;
hidden?: boolean;
rightSlot?: ReactNode; rightSlot?: ReactNode;
} }
| { | {
@@ -97,6 +98,10 @@ export function Tabs({
)} )}
> >
{tabs.map((t) => { {tabs.map((t) => {
if ('hidden' in t && t.hidden) {
return null;
}
const isActive = t.value === value; const isActive = t.value === value;
const btnClassName = classNames( const btnClassName = classNames(
'h-sm flex items-center rounded whitespace-nowrap', 'h-sm flex items-center rounded whitespace-nowrap',

View File

@@ -8,9 +8,13 @@ export interface AppInfo {
appDataDir: string; appDataDir: string;
appLogDir: string; appLogDir: string;
identifier: string; identifier: string;
featureLicense: boolean;
featureUpdater: boolean;
} }
export const appInfo = { export const appInfo = {
...(await invokeCmd('cmd_metadata')), ...(await invokeCmd('cmd_metadata')),
identifier: await getIdentifier(), identifier: await getIdentifier(),
} as AppInfo; } as AppInfo;
console.log('App info', appInfo);