update file manager

This commit is contained in:
Qing
2023-01-01 21:54:05 +08:00
parent 2dd95be90d
commit 39397fc829
16 changed files with 483 additions and 49 deletions

View File

@@ -1,14 +1,13 @@
@import '@radix-ui/colors/blackA.css';
@import '@radix-ui/colors/mauve.css';
@import '@radix-ui/colors/violet.css';
.file-manager-modal {
color: var(--text-color);
height: 90%;
width: 80%;
}
.file-manager {
overflow: auto;
border-radius: 8px;
}
.react-photo-album.react-photo-album--columns {
height: 80vh;
}
@@ -22,6 +21,64 @@
&:hover {
border: 1px solid var(--border-color);
transform: scale(1.01);
transform: scale(1.03);
}
}
.ScrollAreaRoot {
border-radius: 4px;
overflow: hidden;
// box-shadow: 0 2px 10px var(--blackA7);
--scrollbar-size: 10px;
}
.ScrollAreaViewport {
width: 100%;
height: 100%;
border-radius: inherit;
}
.ScrollAreaScrollbar {
display: flex;
/* ensures no selection */
user-select: none;
/* disable browser handling of all panning and zooming gestures on touch devices */
touch-action: none;
padding: 2px;
// background: var(--blackA6);
transition: background 160ms ease-out;
}
.ScrollAreaScrollbar:hover {
background: var(--blackA8);
}
.ScrollAreaScrollbar[data-orientation='vertical'] {
width: var(--scrollbar-size);
}
.ScrollAreaScrollbar[data-orientation='horizontal'] {
flex-direction: column;
height: var(--scrollbar-size);
}
.ScrollAreaThumb {
flex: 1;
background: var(--mauve10);
border-radius: var(--scrollbar-size);
position: relative;
}
/* increase target size for touch devices https://www.w3.org/WAI/WCAG21/Understanding/target-size.html */
.ScrollAreaThumb::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
min-width: 44px;
min-height: 44px;
}
.ScrollAreaCorner {
background: var(--blackA8);
}

View File

@@ -1,6 +1,15 @@
import React, { ReactNode, useEffect, useMemo, useState } from 'react'
import React, {
SyntheticEvent,
useEffect,
useMemo,
useState,
useCallback,
useRef,
} from 'react'
import PhotoAlbum, { RenderPhoto } from 'react-photo-album'
import * as ScrollArea from '@radix-ui/react-scroll-area'
import Modal from '../shared/Modal'
import Button from '../shared/Button'
interface Photo {
src: string
@@ -36,12 +45,35 @@ const renderPhoto: RenderPhoto = ({
interface Props {
show: boolean
onClose: () => void
onPhotoClick: (filename: string) => void
onPhotoClick(filename: string): void
photoWidth: number
}
export default function FileManager(props: Props) {
const { show, onClose, onPhotoClick } = props
const { show, onClose, onPhotoClick, photoWidth } = props
const [filenames, setFileNames] = useState<Filename[]>([])
const [scrollTop, setScrollTop] = useState(0)
const [closeScrollTop, setCloseScrollTop] = useState(0)
useEffect(() => {
if (!show) {
setCloseScrollTop(scrollTop)
}
}, [show, scrollTop])
const onRefChange = useCallback(
(node: HTMLDivElement) => {
if (node !== null) {
if (show) {
setTimeout(() => {
// TODO: without timeout, scrollTo not work, why?
node.scrollTo({ top: closeScrollTop, left: 0 })
}, 100)
}
}
},
[show, closeScrollTop]
)
const onClick = ({ index }: { index: number }) => {
onPhotoClick(filenames[index].name)
@@ -59,9 +91,13 @@ export default function FileManager(props: Props) {
fetchData()
}, [])
const onScroll = (event: SyntheticEvent) => {
setScrollTop(event.currentTarget.scrollTop)
}
const photos = useMemo(() => {
return filenames.map((filename: Filename) => {
const width = 256
const width = photoWidth
const height = filename.height * (width / filename.width)
const src = `/media_thumbnail/${filename.name}?width=${width}&height=${height}`
return { src, height, width }
@@ -71,20 +107,39 @@ export default function FileManager(props: Props) {
return (
<Modal
onClose={onClose}
title="Files"
title={`Files(${photos.length})`}
className="file-manager-modal"
show={show}
>
<div className="file-manager">
<PhotoAlbum
layout="columns"
photos={photos}
renderPhoto={renderPhoto}
spacing={6}
padding={4}
onClick={onClick}
/>
</div>
<ScrollArea.Root className="ScrollAreaRoot">
<ScrollArea.Viewport
className="ScrollAreaViewport"
onScroll={onScroll}
ref={onRefChange}
>
<PhotoAlbum
layout="columns"
photos={photos}
renderPhoto={renderPhoto}
spacing={8}
padding={8}
onClick={onClick}
/>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="ScrollAreaScrollbar"
orientation="vertical"
>
<ScrollArea.Thumb className="ScrollAreaThumb" />
</ScrollArea.Scrollbar>
<ScrollArea.Scrollbar
className="ScrollAreaScrollbar"
orientation="horizontal"
>
<ScrollArea.Thumb className="ScrollAreaThumb" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="ScrollAreaCorner" />
</ScrollArea.Root>
</Modal>
)
}

View File

@@ -4,6 +4,7 @@ import React, { useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import {
enableFileManagerState,
fileState,
isInpaintingState,
isSDState,
@@ -33,14 +34,17 @@ const Header = () => {
const [openMaskPopover, setOpenMaskPopover] = useState(false)
const [showFileManager, setShowFileManager] =
useRecoilState(showFileManagerState)
const enableFileManager = useRecoilValue(enableFileManagerState)
useHotKey(
'f',
() => {
setShowFileManager(!showFileManager)
if (enableFileManager) {
setShowFileManager(!showFileManager)
}
},
{},
[showFileManager]
[showFileManager, enableFileManager]
)
const renderHeader = () => {
@@ -54,15 +58,19 @@ const Header = () => {
gap: 8,
}}
>
<Button
icon={<FolderIcon />}
style={{ border: 0 }}
toolTip="Open File Manager"
tooltipPosition="bottom"
onClick={() => {
setShowFileManager(true)
}}
/>
{enableFileManager ? (
<Button
icon={<FolderIcon />}
style={{ border: 0 }}
toolTip="Open File Manager"
tooltipPosition="bottom"
onClick={() => {
setShowFileManager(true)
}}
/>
) : (
<></>
)}
<label htmlFor={uploadElemId}>
<Button

View File

@@ -120,7 +120,7 @@ function HDSettingBlock() {
return (
<SettingBlock
className="hd-setting-block"
title="High Resolution Strategy"
title="Strategy"
input={
<Selector
width={80}

View File

@@ -15,6 +15,10 @@
.sub-setting-block {
color: var(--text-color);
}
svg {
color: var(--text-color-gray);
}
}
}

View File

@@ -4,11 +4,11 @@
@import './ModelSettingBlock.scss';
.modal-setting {
grid-area: main-content;
background-color: var(--modal-bg);
color: var(--modal-text-color);
box-shadow: 0px 0px 20px rgb(0, 0, 40, 0.2);
width: 600px;
width: 680px;
min-height: 420px;
@include mobile {
display: grid;
@@ -17,4 +17,93 @@
margin-top: -11rem;
animation: slideDown 0.2s ease-out;
}
.modal-header {
padding-left: 8px;
}
}
/* reset */
button,
fieldset,
input {
all: unset;
}
.TabsRoot {
display: flex;
flex-direction: row;
gap: 16px;
width: 100%;
background-color: var(--page-bg);
flex-grow: 1;
}
.TabsList {
display: flex;
flex-direction: column;
gap: 6px;
justify-content: flex-start;
border-right: 1px solid var(--border-color);
background-color: var(--page-bg);
padding-right: 20px;
}
.TabsTrigger {
font-family: inherit;
background-color: white;
padding-top: 8px;
padding-bottom: 8px;
padding-right: 40px;
padding-left: 8px;
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 15px;
line-height: 1;
color: var(--modal-text-color);
user-select: none;
background-color: var(--page-bg);
border-radius: 8px;
}
.TabsTrigger:hover {
background-color: var(--tabs-active-color);
}
.TabsTrigger[data-state='active'] {
background-color: var(--tabs-active-color);
}
.TabsTrigger:focus {
position: relative;
}
.TabsContent {
background-color: white;
outline: none;
background-color: var(--page-bg);
width: 100%;
}
.TabsContent[data-state='active'] {
display: flex;
flex-direction: column;
gap: 14px;
}
.folder-path-block {
display: flex;
flex-direction: column;
gap: 12px;
}
.folder-path {
width: 95%;
border-width: 0;
border-radius: 6px;
padding: 0.3rem 0.5rem;
outline: 1px solid var(--border-color);
&:focus-visible {
border-width: 0;
outline: 1px solid var(--yellow-accent);
}
}

View File

@@ -1,6 +1,8 @@
import React from 'react'
import React, { FormEvent } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { FolderOpenIcon } from '@heroicons/react/24/outline'
import * as Tabs from '@radix-ui/react-tabs'
import {
isPaintByExampleState,
isSDState,
@@ -12,10 +14,23 @@ import HDSettingBlock from './HDSettingBlock'
import ModelSettingBlock from './ModelSettingBlock'
import DownloadMaskSettingBlock from './DownloadMaskSettingBlock'
import useHotKey from '../../hooks/useHotkey'
import SettingBlock from './SettingBlock'
import { Switch, SwitchThumb } from '../shared/Switch'
import Button from '../shared/Button'
import TextInput from '../shared/Input'
declare module 'react' {
interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
// extends React's HTMLAttributes
directory?: string
webkitdirectory?: string
}
}
interface SettingModalProps {
onClose: () => void
}
export default function SettingModal(props: SettingModalProps) {
const { onClose } = props
const [setting, setSettingState] = useRecoilState(settingState)
@@ -47,12 +62,92 @@ export default function SettingModal(props: SettingModalProps) {
className="modal-setting"
show={setting.show}
>
{isSD || isPaintByExample ? <></> : <ManualRunInpaintingSettingBlock />}
{/* <GraduallyInpaintingSettingBlock /> */}
<DownloadMaskSettingBlock />
<ModelSettingBlock />
{isSD ? <></> : <HDSettingBlock />}
<Tabs.Root
className="TabsRoot"
defaultValue="tab1"
orientation="vertical"
>
<Tabs.List className="TabsList">
<Tabs.Trigger className="TabsTrigger" value="tab1">
Model
</Tabs.Trigger>
<Tabs.Trigger className="TabsTrigger" value="tab2">
File
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content className="TabsContent" value="tab1">
{isSD || isPaintByExample ? (
<></>
) : (
<ManualRunInpaintingSettingBlock />
)}
<ModelSettingBlock />
{isSD ? <></> : <HDSettingBlock />}
</Tabs.Content>
<Tabs.Content className="TabsContent" value="tab2">
<DownloadMaskSettingBlock />
<SettingBlock
title="File Manager"
desc="Toggle File Manager"
input={
<Switch
checked={setting.enableFileManager}
onCheckedChange={checked => {
setSettingState(old => {
return { ...old, enableFileManager: checked }
})
}}
>
<SwitchThumb />
</Switch>
}
optionDesc={
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
}}
>
<div className="folder-path-block">
<span>Image directory</span>
<TextInput
disabled={!setting.enableFileManager}
value={setting.imageDirectory}
placeholder="Image directory"
className="folder-path"
onInput={(evt: FormEvent<HTMLInputElement>) => {
evt.preventDefault()
evt.stopPropagation()
const target = evt.target as HTMLInputElement
setSettingState(old => {
return { ...old, imageDirectory: target.value }
})
}}
/>
</div>
<div className="folder-path-block">
<span>Output directory</span>
<TextInput
disabled={!setting.enableFileManager}
value={setting.outputDirectory}
placeholder="Output directory"
className="folder-path"
onInput={(evt: FormEvent<HTMLInputElement>) => {
evt.preventDefault()
evt.stopPropagation()
const target = evt.target as HTMLInputElement
setSettingState(old => {
return { ...old, outputDirectory: target.value }
})
}}
/>
</div>
</div>
}
/>
</Tabs.Content>
</Tabs.Root>
</Modal>
)
}

View File

@@ -58,6 +58,7 @@
.shortcut-description {
justify-self: start;
text-align: left;
font-size: 0.95rem;
@include mobile {
text-align: left;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'
import React, { useEffect, useRef } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import Editor from './Editor/Editor'
import ShortcutsModal from './Shortcuts/ShortcutsModal'
@@ -99,6 +99,7 @@ const Workspace = () => {
{isSD ? <SidePanel /> : <></>}
{isPaintByExample ? <PESidePanel /> : <></>}
<FileManager
photoWidth={256}
show={showFileManager}
onClose={() => {
setShowFileManager(false)

View File

@@ -28,9 +28,9 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: grid;
grid-auto-rows: max-content;
row-gap: 1rem;
display: flex;
flex-direction: column;
gap: 16px;
place-self: center;
padding: 25px;
border-radius: 0.95rem;

View File

@@ -92,6 +92,18 @@ export const showFileManagerState = selector({
},
})
export const enableFileManagerState = selector({
key: 'enableFileManagerState',
get: ({ get }) => {
const app = get(settingState)
return app.enableFileManager
},
set: ({ get, set }, newValue: any) => {
const app = get(settingState)
set(settingState, { ...app, enableFileManager: newValue })
},
})
export const fileState = selector({
key: 'fileState',
get: ({ get }) => {
@@ -245,6 +257,9 @@ export interface Settings {
show: boolean
showCroper: boolean
downloadMask: boolean
enableFileManager: boolean
imageDirectory: string
outputDirectory: string
graduallyInpainting: boolean
runInpaintingManually: boolean
model: AIModel
@@ -373,6 +388,9 @@ export enum SDMode {
export const settingStateDefault: Settings = {
show: false,
showCroper: false,
enableFileManager: false,
imageDirectory: '',
outputDirectory: '',
downloadMask: false,
graduallyInpainting: true,
runInpaintingManually: false,

View File

@@ -14,3 +14,7 @@ a {
color: inherit; /* blue colors for links too */
text-decoration: inherit; /* no underline */
}
input:disabled {
color: var(--text-color-gray);
}

View File

@@ -8,7 +8,7 @@
--yellow-accent: #ffcc00;
--yellow-accent-light: #ffcc0055;
--link-color: rgb(0, 0, 0);
--border-color: rgb(100, 100, 120);
--border-color: rgb(239, 241, 244);
--border-color-light: rgba(100, 100, 120, 0.5);
--tooltip-bg: rgb(230, 230, 234);
--tooltip-text-color: rgb(0, 0, 0);
@@ -60,4 +60,7 @@
0px 2px 1px -1px hsl(205 10.7% 78%), 0 1px hsl(205 10.7% 78%);
--croper-bg: rgba(0, 0, 0, 0.5);
// Tabs
--tabs-active-color: rgb(240 243 249);
}

View File

@@ -8,7 +8,7 @@
--yellow-accent: #ffcc00;
--yellow-accent-light: #ffcc0055;
--link-color: var(--yellow-accent);
--border-color: rgb(100, 100, 120);
--border-color: rgb(30, 30, 30);
--border-color-light: rgba(102, 102, 102);
--tooltip-bg: rgb(33, 33, 33);
--tooltip-text-color: rgb(210, 210, 210);
@@ -29,7 +29,7 @@
// Text
--text-color: white;
--text-color-gray: rgb(138, 143, 152);
--text-color-gray: rgb(195, 196, 198);
// Shared
--btn-text-color: var(--text-color);
@@ -58,4 +58,7 @@
0px 2px 1px -1px hsl(207 5.6% 31.6%), 0 1px hsl(207 5.6% 31.6%);
--croper-bg: rgba(0, 0, 0, 0.5);
// Tabs
--tabs-active-color: rgb(39 40 49);
}