mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 22:40:26 +01:00
Notify of plugin updates and add update UX (#339)
This commit is contained in:
@@ -5,7 +5,10 @@ import { jotaiStore } from '../lib/jotai';
|
||||
import { router } from '../lib/router';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export const openSettings = createFastMutation<void, string, SettingsTab | null>({
|
||||
// Allow tab with optional subtab (e.g., "plugins:installed")
|
||||
type SettingsTabWithSubtab = SettingsTab | `${SettingsTab}:${string}` | null;
|
||||
|
||||
export const openSettings = createFastMutation<void, string, SettingsTabWithSubtab>({
|
||||
mutationKey: ['open_settings'],
|
||||
mutationFn: async (tab) => {
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
@@ -14,7 +17,7 @@ export const openSettings = createFastMutation<void, string, SettingsTab | null>
|
||||
const location = router.buildLocation({
|
||||
to: '/workspaces/$workspaceId/settings',
|
||||
params: { workspaceId },
|
||||
search: { tab: tab ?? undefined },
|
||||
search: { tab: (tab ?? undefined) as SettingsTab | undefined },
|
||||
});
|
||||
|
||||
await invokeCmd('cmd_new_child_window', {
|
||||
|
||||
@@ -45,7 +45,9 @@ export type SettingsTab = (typeof tabs)[number];
|
||||
|
||||
export default function Settings({ hide }: Props) {
|
||||
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
|
||||
const [tab, setTab] = useState<string | undefined>(tabFromQuery);
|
||||
// Parse tab and subtab (e.g., "plugins:installed")
|
||||
const [mainTab, subtab] = tabFromQuery?.split(':') ?? [];
|
||||
const [tab, setTab] = useState<string | undefined>(mainTab || tabFromQuery);
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const plugins = useAtomValue(pluginsAtom);
|
||||
const licenseCheck = useLicense();
|
||||
@@ -118,7 +120,7 @@ export default function Settings({ hide }: Props) {
|
||||
<SettingsTheme />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 px-6 !py-4">
|
||||
<SettingsPlugins />
|
||||
<SettingsPlugins defaultSubtab={tab === TAB_PLUGINS ? subtab : undefined} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 !py-4">
|
||||
<SettingsProxy />
|
||||
|
||||
@@ -43,14 +43,18 @@ function isPluginBundled(plugin: Plugin, vendoredPluginDir: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsPlugins() {
|
||||
interface SettingsPluginsProps {
|
||||
defaultSubtab?: string;
|
||||
}
|
||||
|
||||
export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
|
||||
const [directory, setDirectory] = useState<string | null>(null);
|
||||
const plugins = useAtomValue(pluginsAtom);
|
||||
const bundledPlugins = plugins.filter((p) => isPluginBundled(p, appInfo.vendoredPluginDir));
|
||||
const installedPlugins = plugins.filter((p) => !isPluginBundled(p, appInfo.vendoredPluginDir));
|
||||
const createPlugin = useInstallPlugin();
|
||||
const refreshPlugins = useRefreshPlugins();
|
||||
const [tab, setTab] = useState<string>();
|
||||
const [tab, setTab] = useState<string | undefined>(defaultSubtab);
|
||||
return (
|
||||
<div className="h-full">
|
||||
<Tabs
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import type { InternalEvent, ShowToastRequest } from '@yaakapp-internal/plugins';
|
||||
import type { UpdateInfo, UpdateResponse, YaakNotification } from '@yaakapp-internal/tauri';
|
||||
import { updateAllPlugins } from '@yaakapp-internal/plugins';
|
||||
import type {
|
||||
PluginUpdateNotification,
|
||||
UpdateInfo,
|
||||
UpdateResponse,
|
||||
YaakNotification,
|
||||
} from '@yaakapp-internal/tauri';
|
||||
import { openSettings } from '../commands/openSettings';
|
||||
import { Button } from '../components/core/Button';
|
||||
import { ButtonInfiniteLoading } from '../components/core/ButtonInfiniteLoading';
|
||||
@@ -44,92 +50,164 @@ export function initGlobalListeners() {
|
||||
}
|
||||
});
|
||||
|
||||
const UPDATE_TOAST_ID = 'update-info'; // Share the ID to replace the toast
|
||||
|
||||
listenToTauriEvent<string>('update_installed', async ({ payload: version }) => {
|
||||
showToast({
|
||||
id: UPDATE_TOAST_ID,
|
||||
color: 'primary',
|
||||
timeout: null,
|
||||
message: (
|
||||
<VStack>
|
||||
<h2 className="font-semibold">Yaak {version} was installed</h2>
|
||||
<p className="text-text-subtle text-sm">Start using the new version now?</p>
|
||||
</VStack>
|
||||
),
|
||||
action: ({ hide }) => (
|
||||
<ButtonInfiniteLoading
|
||||
size="xs"
|
||||
className="mr-auto min-w-[5rem]"
|
||||
color="primary"
|
||||
loadingChildren="Restarting..."
|
||||
onClick={() => {
|
||||
hide();
|
||||
setTimeout(() => invokeCmd('cmd_restart', {}), 200);
|
||||
}}
|
||||
>
|
||||
Relaunch Yaak
|
||||
</ButtonInfiniteLoading>
|
||||
),
|
||||
});
|
||||
console.log('Got update installed event', version);
|
||||
showUpdateInstalledToast(version);
|
||||
});
|
||||
|
||||
// Listen for update events
|
||||
listenToTauriEvent<UpdateInfo>(
|
||||
'update_available',
|
||||
async ({ payload: { version, replyEventId, downloaded } }) => {
|
||||
console.log('Received update available event', { replyEventId, version, downloaded });
|
||||
jotaiStore.set(updateAvailableAtom, { version, downloaded });
|
||||
|
||||
// Acknowledge the event, so we don't time out and try the fallback update logic
|
||||
await emit<UpdateResponse>(replyEventId, { type: 'ack' });
|
||||
|
||||
showToast({
|
||||
id: UPDATE_TOAST_ID,
|
||||
color: 'info',
|
||||
timeout: null,
|
||||
message: (
|
||||
<VStack>
|
||||
<h2 className="font-semibold">Yaak {version} is available</h2>
|
||||
<p className="text-text-subtle text-sm">
|
||||
{downloaded ? 'Do you want to install' : 'Download and install'} the update?
|
||||
</p>
|
||||
</VStack>
|
||||
),
|
||||
action: () => (
|
||||
<HStack space={1.5}>
|
||||
<ButtonInfiniteLoading
|
||||
size="xs"
|
||||
color="info"
|
||||
className="min-w-[10rem]"
|
||||
loadingChildren={downloaded ? 'Installing...' : 'Downloading...'}
|
||||
onClick={async () => {
|
||||
await emit<UpdateResponse>(replyEventId, { type: 'action', action: 'install' });
|
||||
}}
|
||||
>
|
||||
{downloaded ? 'Install Now' : 'Download and Install'}
|
||||
</ButtonInfiniteLoading>
|
||||
<Button
|
||||
size="xs"
|
||||
color="info"
|
||||
variant="border"
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
onClick={async () => {
|
||||
await openUrl(`https://yaak.app/changelog/${version}`);
|
||||
}}
|
||||
>
|
||||
What's New
|
||||
</Button>
|
||||
</HStack>
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
listenToTauriEvent<UpdateInfo>('update_available', async ({ payload }) => {
|
||||
console.log('Got update available', payload);
|
||||
showUpdateAvailableToast(payload);
|
||||
});
|
||||
|
||||
listenToTauriEvent<YaakNotification>('notification', ({ payload }) => {
|
||||
console.log('Got notification event', payload);
|
||||
showNotificationToast(payload);
|
||||
});
|
||||
|
||||
// Listen for plugin update events
|
||||
listenToTauriEvent<PluginUpdateNotification>('plugin_updates_available', ({ payload }) => {
|
||||
console.log('Got plugin updates event', payload);
|
||||
showPluginUpdatesToast(payload);
|
||||
});
|
||||
}
|
||||
|
||||
function showUpdateInstalledToast(version: string) {
|
||||
const UPDATE_TOAST_ID = 'update-info';
|
||||
|
||||
showToast({
|
||||
id: UPDATE_TOAST_ID,
|
||||
color: 'primary',
|
||||
timeout: null,
|
||||
message: (
|
||||
<VStack>
|
||||
<h2 className="font-semibold">Yaak {version} was installed</h2>
|
||||
<p className="text-text-subtle text-sm">Start using the new version now?</p>
|
||||
</VStack>
|
||||
),
|
||||
action: ({ hide }) => (
|
||||
<ButtonInfiniteLoading
|
||||
size="xs"
|
||||
className="mr-auto min-w-[5rem]"
|
||||
color="primary"
|
||||
loadingChildren="Restarting..."
|
||||
onClick={() => {
|
||||
hide();
|
||||
setTimeout(() => invokeCmd('cmd_restart', {}), 200);
|
||||
}}
|
||||
>
|
||||
Relaunch Yaak
|
||||
</ButtonInfiniteLoading>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
|
||||
const UPDATE_TOAST_ID = 'update-info';
|
||||
const { version, replyEventId, downloaded } = updateInfo;
|
||||
|
||||
jotaiStore.set(updateAvailableAtom, { version, downloaded });
|
||||
|
||||
// Acknowledge the event, so we don't time out and try the fallback update logic
|
||||
await emit<UpdateResponse>(replyEventId, { type: 'ack' });
|
||||
|
||||
showToast({
|
||||
id: UPDATE_TOAST_ID,
|
||||
color: 'info',
|
||||
timeout: null,
|
||||
message: (
|
||||
<VStack>
|
||||
<h2 className="font-semibold">Yaak {version} is available</h2>
|
||||
<p className="text-text-subtle text-sm">
|
||||
{downloaded ? 'Do you want to install' : 'Download and install'} the update?
|
||||
</p>
|
||||
</VStack>
|
||||
),
|
||||
action: () => (
|
||||
<HStack space={1.5}>
|
||||
<ButtonInfiniteLoading
|
||||
size="xs"
|
||||
color="info"
|
||||
className="min-w-[10rem]"
|
||||
loadingChildren={downloaded ? 'Installing...' : 'Downloading...'}
|
||||
onClick={async () => {
|
||||
await emit<UpdateResponse>(replyEventId, { type: 'action', action: 'install' });
|
||||
}}
|
||||
>
|
||||
{downloaded ? 'Install Now' : 'Download and Install'}
|
||||
</ButtonInfiniteLoading>
|
||||
<Button
|
||||
size="xs"
|
||||
color="info"
|
||||
variant="border"
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
onClick={async () => {
|
||||
await openUrl(`https://yaak.app/changelog/${version}`);
|
||||
}}
|
||||
>
|
||||
What's New
|
||||
</Button>
|
||||
</HStack>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function showPluginUpdatesToast(updateInfo: PluginUpdateNotification) {
|
||||
const PLUGIN_UPDATE_TOAST_ID = 'plugin-updates';
|
||||
const count = updateInfo.updateCount;
|
||||
const pluginNames = updateInfo.plugins.map((p: { name: string }) => p.name);
|
||||
|
||||
showToast({
|
||||
id: PLUGIN_UPDATE_TOAST_ID,
|
||||
color: 'info',
|
||||
timeout: null,
|
||||
message: (
|
||||
<VStack>
|
||||
<h2 className="font-semibold">
|
||||
{count === 1 ? '1 plugin update' : `${count} plugin updates`} available
|
||||
</h2>
|
||||
<p className="text-text-subtle text-sm">
|
||||
{count === 1
|
||||
? pluginNames[0]
|
||||
: `${pluginNames.slice(0, 2).join(', ')}${count > 2 ? `, and ${count - 2} more` : ''}`}
|
||||
</p>
|
||||
</VStack>
|
||||
),
|
||||
action: ({ hide }) => (
|
||||
<HStack space={1.5}>
|
||||
<ButtonInfiniteLoading
|
||||
size="xs"
|
||||
color="info"
|
||||
className="min-w-[5rem]"
|
||||
loadingChildren="Updating..."
|
||||
onClick={async () => {
|
||||
const updated = await updateAllPlugins();
|
||||
hide();
|
||||
if (updated.length > 0) {
|
||||
showToast({
|
||||
color: 'success',
|
||||
message: `Successfully updated ${updated.length} plugin${updated.length === 1 ? '' : 's'}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Update All
|
||||
</ButtonInfiniteLoading>
|
||||
<Button
|
||||
size="xs"
|
||||
color="info"
|
||||
variant="border"
|
||||
onClick={() => {
|
||||
hide();
|
||||
openSettings.mutate('plugins:installed');
|
||||
}}
|
||||
>
|
||||
View Updates
|
||||
</Button>
|
||||
</HStack>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function showNotificationToast(n: YaakNotification) {
|
||||
|
||||
Reference in New Issue
Block a user