mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-05 13:10:24 +00:00
Reworks the app mode display for mobile devices. Adds multiple bottom tabs that can be swiped between.  To be handled in followup PRs - Nicer error display - Support for even smaller screens - UX improvements for the 'Outputs' pane - Was postponed to minimize conflicts with non-mobile development. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9047-App-mode-mobile-redesign-30e6d73d365081388e4adea4df886522) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
229 lines
7.5 KiB
Vue
229 lines
7.5 KiB
Vue
<script setup lang="ts">
|
|
import type { MenuItem } from 'primevue/menuitem'
|
|
import { useFullscreen, usePointerSwipe } from '@vueuse/core'
|
|
import { computed, ref, useTemplateRef } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import DropdownMenu from '@/components/common/DropdownMenu.vue'
|
|
import AssetsSidebarTab from '@/components/sidebar/tabs/AssetsSidebarTab.vue'
|
|
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import Popover from '@/components/ui/Popover.vue'
|
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
|
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
import LinearControls from '@/renderer/extensions/linearMode/LinearControls.vue'
|
|
import LinearPreview from '@/renderer/extensions/linearMode/LinearPreview.vue'
|
|
import { useColorPaletteService } from '@/services/colorPaletteService'
|
|
import { useQueueStore } from '@/stores/queueStore'
|
|
import { useMenuItemStore } from '@/stores/menuItemStore'
|
|
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
const tabs = [
|
|
['linearMode.mobileControls', 'icon-[lucide--play]'],
|
|
['nodeHelpPage.outputs', 'icon-[comfy--image-ai-edit]'],
|
|
['sideToolbar.assets', 'icon-[lucide--images]']
|
|
]
|
|
|
|
const canvasStore = useCanvasStore()
|
|
const colorPaletteService = useColorPaletteService()
|
|
const colorPaletteStore = useColorPaletteStore()
|
|
const { isLoggedIn } = useCurrentUser()
|
|
const { t } = useI18n()
|
|
const { commandIdToMenuItem } = useMenuItemStore()
|
|
const queueStore = useQueueStore()
|
|
const workflowService = useWorkflowService()
|
|
const workflowStore = useWorkflowStore()
|
|
const { toggle: toggleFullscreen } = useFullscreen(undefined, {
|
|
autoExit: true
|
|
})
|
|
|
|
const activeIndex = ref(2)
|
|
const sliderPaneRef = useTemplateRef('sliderPaneRef')
|
|
const sliderWidth = computed(() => sliderPaneRef.value?.offsetWidth)
|
|
|
|
const { distanceX, isSwiping } = usePointerSwipe(sliderPaneRef, {
|
|
disableTextSelect: true,
|
|
onSwipeEnd() {
|
|
if (
|
|
!sliderWidth.value ||
|
|
Math.abs(distanceX.value) / sliderWidth.value < 0.4
|
|
)
|
|
return
|
|
if (distanceX.value < 0)
|
|
activeIndex.value = Math.max(activeIndex.value - 1, 0)
|
|
else activeIndex.value = Math.min(activeIndex.value + 1, tabs.length - 1)
|
|
}
|
|
})
|
|
|
|
const translate = computed(() => {
|
|
const slideOffset =
|
|
isSwiping.value && sliderWidth.value
|
|
? distanceX.value / sliderWidth.value
|
|
: 0
|
|
const totalOffset = slideOffset + activeIndex.value
|
|
return `${totalOffset * -100}vw`
|
|
})
|
|
|
|
function onClick(index: number) {
|
|
if (Math.abs(distanceX.value) > 30) return
|
|
activeIndex.value = index
|
|
}
|
|
|
|
const workflowsEntries = computed(() => {
|
|
return workflowStore.openWorkflows.map((w) => ({
|
|
label: w.filename,
|
|
icon: w.activeState?.extra?.linearMode
|
|
? 'icon-[lucide--panels-top-left] bg-primary-background'
|
|
: undefined,
|
|
command: () => workflowService.openWorkflow(w)
|
|
}))
|
|
})
|
|
|
|
const menuEntries = computed<MenuItem[]>(() => [
|
|
{
|
|
label: t('linearMode.appModeToolbar.apps'),
|
|
icon: 'icon-[lucide--panels-top-left]'
|
|
},
|
|
{
|
|
...commandIdToMenuItem('Comfy.BrowseTemplates'),
|
|
label: t('sideToolbar.templates'),
|
|
icon: 'icon-[comfy--template]'
|
|
},
|
|
{ separator: true },
|
|
{
|
|
label: t('icon.file'),
|
|
items: [
|
|
commandIdToMenuItem('Comfy.RenameWorkflow'),
|
|
commandIdToMenuItem('Comfy.DuplicateWorkflow'),
|
|
{ separator: true },
|
|
commandIdToMenuItem('Comfy.SaveWorkflow'),
|
|
commandIdToMenuItem('Comfy.SaveWorkflowAs'),
|
|
{ separator: true },
|
|
commandIdToMenuItem('Comfy.ExportWorkflow'),
|
|
commandIdToMenuItem('Comfy.ExportWorkflowAPI')
|
|
]
|
|
},
|
|
{
|
|
label: t('g.edit'),
|
|
items: [
|
|
commandIdToMenuItem('Comfy.Undo'),
|
|
commandIdToMenuItem('Comfy.Redo'),
|
|
{ separator: true },
|
|
commandIdToMenuItem('Comfy.RefreshNodeDefinitions'),
|
|
commandIdToMenuItem('Comfy.Memory.UnloadModels'),
|
|
commandIdToMenuItem('Comfy.Memory.UnloadModelsAndExecutionCache')
|
|
]
|
|
},
|
|
{
|
|
label: t('linearMode.enterNodeGraph'),
|
|
icon: 'icon-[comfy--workflow]',
|
|
new: true,
|
|
command: () => (canvasStore.linearMode = false)
|
|
},
|
|
{ separator: true },
|
|
{
|
|
label: t('menu.theme'),
|
|
items: colorPaletteStore.palettes.map((palette) => ({
|
|
label: palette.name,
|
|
icon:
|
|
colorPaletteStore.activePaletteId === palette.id
|
|
? 'icon-[lucide--check]'
|
|
: '',
|
|
command: () => colorPaletteService.loadColorPalette(palette.id)
|
|
}))
|
|
},
|
|
{ separator: true },
|
|
{
|
|
...commandIdToMenuItem('Comfy.ShowSettingsDialog'),
|
|
label: t('menu.settings')
|
|
},
|
|
{ ...commandIdToMenuItem('Comfy.ToggleHelpCenter'), label: t('menu.help') },
|
|
{
|
|
label: t('menu.fullscreen'),
|
|
icon: 'icon-[lucide--fullscreen]',
|
|
command: toggleFullscreen
|
|
}
|
|
])
|
|
</script>
|
|
<template>
|
|
<section class="absolute w-full h-full flex flex-col bg-secondary-background">
|
|
<header
|
|
class="w-full h-16 px-4 py-3 flex border-border-subtle border-b items-center gap-3 bg-base-background"
|
|
>
|
|
<DropdownMenu :entries="menuEntries" />
|
|
<Popover
|
|
:entries="workflowsEntries"
|
|
class="w-(--reka-popover-content-available-width)"
|
|
:collision-padding="20"
|
|
>
|
|
<template #button>
|
|
<!--TODO: Use button here? Probably too much work to destyle-->
|
|
<div
|
|
class="bg-secondary-background h-10 rounded-sm grow-1 flex items-center p-2 gap-2"
|
|
>
|
|
<i
|
|
class="shrink-0 icon-[lucide--panels-top-left] bg-primary-background"
|
|
/>
|
|
<span
|
|
class="truncate contain-size h-full w-full"
|
|
v-text="workflowStore.activeWorkflow?.filename"
|
|
/>
|
|
<i
|
|
class="shrink-0 icon-[lucide--chevron-down] bg-muted-foreground"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Popover>
|
|
<CurrentUserButton v-if="isLoggedIn" :show-arrow="false" />
|
|
</header>
|
|
<div class="size-full contain-content rounded-b-4xl">
|
|
<div
|
|
:class="
|
|
cn('size-full relative', !isSwiping && 'transition-[translate]')
|
|
"
|
|
:style="{ translate }"
|
|
>
|
|
<div class="overflow-y-auto contain-size h-full w-screen absolute">
|
|
<LinearControls mobile @navigate-assets="activeIndex = 2" />
|
|
</div>
|
|
<div
|
|
class="w-screen absolute h-full bg-base-background left-[100vw] top-0 flex flex-col"
|
|
>
|
|
<LinearPreview mobile />
|
|
</div>
|
|
<AssetsSidebarTab
|
|
class="h-full w-screen absolute bg-base-background left-[200vw] top-0"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
ref="sliderPaneRef"
|
|
class="h-22 p-4 w-full flex gap-4 items-center justify-around bg-secondary-background"
|
|
>
|
|
<Button
|
|
v-for="([label, icon], index) in tabs"
|
|
:key="label"
|
|
:variant="index === activeIndex ? 'secondary' : 'muted-textonly'"
|
|
class="flex-col h-14 grow-1"
|
|
@click="onClick(index)"
|
|
>
|
|
<div class="relative size-4">
|
|
<i :class="cn('size-4', icon)" />
|
|
<div
|
|
v-if="
|
|
index === 1 &&
|
|
(queueStore.runningTasks.length > 0 ||
|
|
queueStore.pendingTasks.length > 0)
|
|
"
|
|
class="absolute bg-primary-background size-2 -top-1 -right-1 rounded-full animate-pulse"
|
|
/>
|
|
</div>
|
|
{{ t(label) }}
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</template>
|