Skip to content

Commit

Permalink
feat: refactor app-download hook to support binding (#7)
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Wang <i@ryanc.cc>
  • Loading branch information
ruibaby committed Sep 8, 2023
1 parent 527e902 commit 0ca9ff8
Show file tree
Hide file tree
Showing 11 changed files with 365 additions and 121 deletions.
2 changes: 1 addition & 1 deletion console/postcss.config.js
Expand Up @@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};
81 changes: 81 additions & 0 deletions console/src/components/AppActionButton.vue
@@ -0,0 +1,81 @@
<script lang="ts" setup>
import { useAppCompare } from "@/composables/use-app-compare";
import { useAppDownload } from "@/composables/use-app-download";
import type { ApplicationSearchResult } from "@/types";
import { VButton } from "@halo-dev/components";
import { computed, toRefs } from "vue";
const props = withDefaults(
defineProps<{
app?: ApplicationSearchResult;
size?: string;
}>(),
{
app: undefined,
size: "default",
}
);
const { app } = toRefs(props);
const { installing, handleInstall } = useAppDownload(app);
const { isSatisfies, hasInstalled } = useAppCompare(app);
const actions = computed(() => {
return [
{
label: installing?.value ? "安装中" : "安装",
type: "default",
available:
!hasInstalled.value && isSatisfies.value && app.value?.application.spec.priceConfig?.mode !== "ONE_TIME",
onClick: handleInstall,
loading: installing?.value,
disabled: false,
},
{
label: `¥${(app.value?.application.spec.priceConfig?.oneTimePrice || 0) / 100}`,
type: "default",
// TODO: 需要判断是否已经购买
available: app.value?.application.spec.priceConfig?.mode === "ONE_TIME" && !hasInstalled.value,
onClick: () => {
window.open(`https://halo.run/store/apps/${app.value?.application.metadata.name}/buy`);
},
loading: false,
disabled: false,
},
{
label: "已安装",
type: "default",
available: hasInstalled.value,
onClick: undefined,
loading: false,
disabled: true,
},
{
label: "版本不兼容",
type: "default",
available: !isSatisfies.value && !hasInstalled.value,
onClick: undefined,
loading: false,
disabled: true,
},
];
});
const action = computed(() => {
return actions.value.find((action) => action.available);
});
</script>

<template>
<VButton
v-if="action"
:size="size"
:type="action.type"
:disabled="action.disabled"
:loading="action.loading"
@click="action.onClick"
>
{{ action.label }}
</VButton>
</template>
16 changes: 2 additions & 14 deletions console/src/components/AppCard.vue
@@ -1,12 +1,11 @@
<script lang="ts" setup>
import { useAppControl } from "@/composables/use-app-control";
import type { ApplicationSearchResult } from "@/types";
import { relativeTimeTo } from "@/utils/date";
import { VButton } from "@halo-dev/components";
import { toRefs } from "vue";
import { computed } from "vue";
import { prependDomain } from "@/utils/resource";
import AppVersionCheckBar from "./AppVersionCheckBar.vue";
import AppActionButton from "./AppActionButton.vue";
const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -40,8 +39,6 @@ const vendor = computed(() => {
logo: props.app.owner.avatar,
};
});
const { action } = useAppControl(app);
</script>

<template>
Expand Down Expand Up @@ -155,16 +152,7 @@ const { action } = useAppControl(app);
</span>
</div>
<div>
<VButton
v-if="action"
size="sm"
:type="action.type"
:disabled="action.disabled"
:loading="action.loading"
@click="action.onClick"
>
{{ action.label }}
</VButton>
<AppActionButton :app="app" size="sm" />
</div>
</div>
</div>
Expand Down
17 changes: 4 additions & 13 deletions console/src/components/AppDetailModal.vue
@@ -1,17 +1,18 @@
<script lang="ts" setup>
import { IconLink, VButton, VLoading, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
import { IconLink, VLoading, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query";
import { ref } from "vue";
import { computed } from "vue";
import { toRefs } from "vue";
import { prependDomain } from "@/utils/resource";
import type { ApplicationSearchResult } from "@/types";
import storeApiClient from "@/utils/store-api-client";
import { useAppControl } from "@/composables/use-app-control";
import DetailSidebar from "./detail/DetailSidebar.vue";
import DetailReadme from "./detail/DetailReadme.vue";
import DetailReleases from "./detail/DetailReleases.vue";
import AppVersionCheckBar from "./AppVersionCheckBar.vue";
import AppActionButton from "./AppActionButton.vue";
const props = withDefaults(
defineProps<{
visible: boolean;
Expand Down Expand Up @@ -65,8 +66,6 @@ const title = computed(() => {
});
const activeId = ref(props.tab);
const { action } = useAppControl(app);
</script>

<template>
Expand Down Expand Up @@ -141,15 +140,7 @@ const { action } = useAppControl(app);
</div>
<template #footer>
<VSpace>
<VButton
v-if="action"
:type="action.type"
:disabled="action.disabled"
:loading="action.loading"
@click="action.onClick"
>
{{ action.label }}
</VButton>
<AppActionButton :app="app" />
<AppVersionCheckBar :app="app" />
</VSpace>
</template>
Expand Down
4 changes: 2 additions & 2 deletions console/src/components/AppVersionCheckBar.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import AppDetailModal from "./AppDetailModal.vue";
import { nextTick, ref, toRefs } from "vue";
import { useAppControl } from "@/composables/use-app-control";
import { useAppDownload } from "@/composables/use-app-download";
import RiArrowUpCircleLine from "~icons/ri/arrow-up-circle-line";
import type { ApplicationSearchResult } from "@/types";
import { useAppCompare } from "@/composables/use-app-compare";
Expand All @@ -19,7 +19,7 @@ const { app } = toRefs(props);
const { hasUpdate, isSatisfies } = useAppCompare(app);
const { upgrading, handleUpgrade } = useAppControl(app);
const { upgrading, handleUpgrade } = useAppDownload(app);
const detailModal = ref(false);
const detailModalVisible = ref(false);
Expand Down
4 changes: 2 additions & 2 deletions console/src/components/ThemeOrPluginVersionCheckBar.vue
Expand Up @@ -3,7 +3,7 @@ import type { Plugin, Theme } from "@halo-dev/api-client";
import AppDetailModal from "./AppDetailModal.vue";
import { nextTick, ref, toRefs } from "vue";
import { usePluginVersion } from "@/composables/use-plugin-version";
import { useAppControl } from "@/composables/use-app-control";
import { useAppDownload } from "@/composables/use-app-download";
import RiArrowUpCircleLine from "~icons/ri/arrow-up-circle-line";
import { useThemeVersion } from "@/composables/use-theme-version";
import { useAppCompare } from "@/composables/use-app-compare";
Expand Down Expand Up @@ -38,7 +38,7 @@ function useVersion() {
const { hasUpdate, matchedApp } = useVersion();
const { isSatisfies } = useAppCompare(matchedApp);
const { upgrading, handleUpgrade } = useAppControl(matchedApp);
const { upgrading, handleUpgrade } = useAppDownload(matchedApp);
const detailModal = ref(false);
const detailModalVisible = ref(false);
Expand Down
159 changes: 154 additions & 5 deletions console/src/components/detail/DetailReleaseItem.vue
@@ -1,10 +1,18 @@
<script lang="ts" setup>
import { useAppControl } from "@/composables/use-app-control";
import type { ApplicationSearchResult, ReleaseDetail } from "@/types";
import { useAppCompare } from "@/composables/use-app-compare";
import { useAppDownload } from "@/composables/use-app-download";
import { useHaloVersion } from "@/composables/use-halo-version";
import type { ApplicationReleaseAsset, ApplicationSearchResult, ReleaseDetail } from "@/types";
import { relativeTimeTo } from "@/utils/date";
import prettyBytes from "pretty-bytes";
import { toRefs } from "vue";
import { computed, toRefs } from "vue";
import TablerCloudDownload from "~icons/tabler/cloud-download";
import semver from "semver";
import { useMutation } from "@tanstack/vue-query";
import { apiClient } from "@/utils/api-client";
import { Dialog, Toast } from "@halo-dev/components";
import type { PluginInstallationErrorResponse, ThemeInstallationErrorResponse } from "@/types/core";
import type { AxiosError } from "axios";
const props = withDefaults(
defineProps<{
Expand All @@ -18,7 +26,145 @@ const props = withDefaults(
const { app } = toRefs(props);
const { handleInstall, installing } = useAppControl(app);
const { haloVersion } = useHaloVersion();
const {
checkPluginUpgradeStatus,
checkThemeUpgradeStatus,
handleBindingPluginAppId,
handleBindingThemeAppId,
handleClearQueryCache,
handleForceUpgradePlugin,
handleForceUpgradeTheme,
} = useAppDownload(app);
const { matchedPlugin, matchedTheme, appType, hasInstalled: appHasInstalled } = useAppCompare(app);
const hasInstalled = computed(() => {
if (appType.value === "PLUGIN") {
return matchedPlugin.value?.spec.version === props.release.release.spec.version;
}
if (appType.value === "THEME") {
return matchedTheme.value?.spec.version === props.release.release.spec.version;
}
return false;
});
const isSatisfies = computed(() => {
const { requires } = props.release.release.spec;
if (!haloVersion.value || !requires) return false;
return semver.satisfies(haloVersion.value, requires, { includePrerelease: true });
});
function getDownloadUrl(asset: ApplicationReleaseAsset) {
return `${import.meta.env.VITE_APP_STORE_BACKEND}/store/apps/${
app.value?.application.metadata.name
}/releases/download/${props.release.release.metadata.name}/assets/${asset.metadata.name}`;
}
const { isLoading: installing, mutate: handleInstall } = useMutation({
mutationKey: ["install-app-from-release"],
mutationFn: async ({ asset }: { asset: ApplicationReleaseAsset }) => {
const { version: releaseVersion } = props.release.release.spec;
const { version: currentVersion } = matchedPlugin.value?.spec || matchedTheme.value?.spec || {};
const downloadUrl = getDownloadUrl(asset);
if (appType.value === "PLUGIN") {
if (appHasInstalled.value) {
if (semver.gt(releaseVersion || "*", currentVersion || "*")) {
await handleForceUpgradePlugin(
matchedPlugin.value?.metadata.name as string,
downloadUrl,
props.release.release.spec.version
);
} else {
Dialog.warning({
title: "当前已安装较新的版本",
description: "确定要安装一个旧版本吗?",
async onConfirm() {
await handleForceUpgradePlugin(
matchedPlugin.value?.metadata.name as string,
downloadUrl,
props.release.release.spec.version
);
},
});
}
} else {
const { data: plugin } = await apiClient.plugin.installPluginFromUri({
installFromUriRequest: { uri: downloadUrl },
});
if (await checkPluginUpgradeStatus(plugin, props.release.release.spec.version)) {
await handleBindingPluginAppId({ plugin: plugin });
Toast.success("安装成功");
handleClearQueryCache();
}
}
return;
}
if (appType.value === "THEME") {
if (appHasInstalled.value) {
if (semver.gt(releaseVersion || "*", currentVersion || "*")) {
await handleForceUpgradeTheme(
matchedTheme.value?.metadata.name as string,
downloadUrl,
props.release.release.spec.version
);
} else {
Dialog.warning({
title: "当前已安装较新的版本",
description: "确定要安装一个旧版本吗?",
async onConfirm() {
await handleForceUpgradeTheme(
matchedTheme.value?.metadata.name as string,
downloadUrl,
props.release.release.spec.version
);
},
});
}
} else {
const { data: theme } = await apiClient.theme.installThemeFromUri({
installFromUriRequest: { uri: downloadUrl },
});
if (await checkThemeUpgradeStatus(theme, props.release.release.spec.version)) {
await handleBindingThemeAppId({ theme });
Toast.success("安装成功");
handleClearQueryCache();
}
}
}
},
onError(error: AxiosError<PluginInstallationErrorResponse | ThemeInstallationErrorResponse>, variables) {
Dialog.warning({
title: `当前${appType.value === "PLUGIN" ? "插件" : "主题"}已经安装,是否重新安装?`,
description:
"请确认当前安装的应用是否和已存在的应用一致,重新安装之后会记录应用的安装来源,后续可以通过应用市场进行升级。",
onConfirm: async () => {
if (!error.response?.data) {
return;
}
const downloadUrl = getDownloadUrl(variables.asset);
if ("pluginName" in error.response.data) {
await handleForceUpgradePlugin(
error.response.data.pluginName,
downloadUrl,
props.release.release.spec.version
);
return;
}
if ("themeName" in error.response.data) {
await handleForceUpgradeTheme(error.response.data.themeName, downloadUrl, props.release.release.spec.version);
return;
}
},
});
},
});
</script>

<template>
Expand Down Expand Up @@ -76,10 +222,13 @@ const { handleInstall, installing } = useAppControl(app);
</span>
</div>
<div>
<span v-if="hasInstalled" class="as-text-sm as-text-gray-600"> 已安装 </span>
<span v-else-if="!isSatisfies" class="as-text-sm as-text-gray-600"> 不兼容 </span>
<span
v-else
class="as-text-sm as-text-blue-600 hover:as-text-blue-500"
:class="{ 'as-pointer-events-none': installing }"
@click="handleInstall()"
@click="handleInstall({ asset })"
>
{{ installing ? "安装中" : "安装" }}
</span>
Expand Down

0 comments on commit 0ca9ff8

Please sign in to comment.