Skip to content

Commit

Permalink
refactor: todo item inline input, feat: add dark mode support applica…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
aliosmandev committed Aug 4, 2024
1 parent 5d745c2 commit 396ae44
Show file tree
Hide file tree
Showing 10 changed files with 80 additions and 124 deletions.
Binary file added build/menubar-icon-dakr@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added build/menubar-icon-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified build/menubar-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified build/menubar-icon@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ files:
extraResources:
- build/menubar-icon.png
- build/menubar-icon@2x.png
- build/menubar-icon-dark.png
- build/menubar-icon-dark@2x.png
asarUnpack:
- resources/**
win:
Expand Down
6 changes: 4 additions & 2 deletions src/main/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import path from 'path'

export const ICON_PATH_DEV = path.join(__dirname, '..', '..', 'build', 'menubar-icon.png')
export const ICON_PATH_DEV = path.join(__dirname, '..', '..', 'build', 'menubar-icon-dark.png')
export const DARK_ICON_PATH_DEV = path.join(__dirname, '..', '..', 'build', 'menubar-icon.png')

export const ICON_PATH_PROD = path.join(process.resourcesPath, 'build', 'menubar-icon.png')
export const ICON_PATH_PROD = path.join(process.resourcesPath, 'build', 'menubar-icon-dark.png')
export const DARK_ICON_PATH_PROD = path.join(process.resourcesPath, 'build', 'menubar-icon.png')

export const INDEX_HTML_PATH = require('url').format({
protocol: 'file',
Expand Down
17 changes: 9 additions & 8 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow, ipcMain, nativeImage, shell, Tray } from 'electron'
import { app, BrowserWindow, ipcMain, nativeTheme, shell, Tray } from 'electron'
import { Menubar, menubar } from 'menubar'
import { join } from 'path'
import { ICON_PATH_DEV, ICON_PATH_PROD, INDEX_HTML_PATH } from './constants'
import { INDEX_HTML_PATH } from './constants'
import { addContextmenu } from './menu'

const iconPath = app.isPackaged ? ICON_PATH_PROD : ICON_PATH_DEV
const icon = nativeImage.createFromPath(iconPath)
import { getTrayIcon, onThemeChange } from './menu/theme'

let mb: Menubar | null = null
const icon = getTrayIcon()

function createWindow(): void {
mb = menubar({
Expand All @@ -35,11 +34,13 @@ function createWindow(): void {
})

mb?.on('ready', () => {
if (mb) {
addContextmenu(mb)
}
addContextmenu(mb!)
nativeTheme.on('updated', () => {
onThemeChange(mb!)
})
})

// set tray & dock images here
app.on('open-url', (event, url) => {
event.preventDefault()
const authCode = new URL(url).searchParams.get('accessToken')
Expand Down
21 changes: 21 additions & 0 deletions src/main/menu/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { app, nativeImage, nativeTheme } from 'electron'
import { Menubar } from 'menubar'
import {
DARK_ICON_PATH_DEV,
DARK_ICON_PATH_PROD,
ICON_PATH_DEV,
ICON_PATH_PROD
} from '../constants'

export const getTrayIcon = () => {
const isDark = nativeTheme.shouldUseDarkColors
return app.isPackaged
? nativeImage.createFromPath(isDark ? DARK_ICON_PATH_PROD : ICON_PATH_PROD)
: nativeImage.createFromPath(isDark ? DARK_ICON_PATH_DEV : ICON_PATH_DEV)
}

export const onThemeChange = (mb: Menubar) => {
const icon = getTrayIcon()

mb?.tray?.setImage(icon)
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ export const TodoItem = ({ blockId, text, completed }: NodeItemType) => {
deleteMutate()
}

console.log(isCompleted)

return (
<ContextMenu>
<ContextMenuTrigger
Expand All @@ -91,7 +89,7 @@ export const TodoItem = ({ blockId, text, completed }: NodeItemType) => {
/>
)}
<InlineInput
value={text || ''}
defaultValue={text || ''}
onChange={(value) => handleUpdateTodo(blockId, undefined, value)}
placeholder="To-do"
editable={true}
Expand Down
154 changes: 43 additions & 111 deletions src/renderer/src/components/ui/inline-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cn } from '@nextui-org/react'
import { useEffect, useRef, useState } from 'react'

export interface InlineInputProps {
value: string | undefined
defaultValue: string | undefined
onChange: (value: string) => void
editable?: boolean
placeholder?: string
Expand All @@ -11,101 +11,52 @@ export interface InlineInputProps {
customEditing?: boolean
}

const useCalculateRows = (width: number) => {
const dummyDivRef = useRef<HTMLDivElement | null>(null)

useEffect(() => {
dummyDivRef.current = document.createElement('div')
dummyDivRef.current.style.width = `${width}px`
dummyDivRef.current.style.visibility = 'hidden'
dummyDivRef.current.style.whiteSpace = 'pre-wrap'
dummyDivRef.current.style.wordWrap = 'break-word'
dummyDivRef.current.style.position = 'absolute'
dummyDivRef.current.style.pointerEvents = 'none'
dummyDivRef.current.style.zIndex = '-9999'
dummyDivRef.current.style.fontFamily = 'inherit'
dummyDivRef.current.style.fontSize = 'inherit'
dummyDivRef.current.style.lineHeight = 'inherit'
document.body.appendChild(dummyDivRef.current)

return () => {
if (dummyDivRef.current) {
document.body.removeChild(dummyDivRef.current)
}
}
}, [width])

const calculateRows = (text: string) => {
if (dummyDivRef.current) {
dummyDivRef.current.innerText = text
const rows = dummyDivRef.current.clientHeight / 25 // 20 is an approximate line height
return Math.ceil(rows)
}
return 1
}

return calculateRows
}

export const InlineInput = ({
value,
defaultValue,
onChange,
placeholder,
endContent,
className,
customEditing = false
}: InlineInputProps) => {
const [isEditing, setIsEditing] = useState(customEditing)
const [inputValue, setInputValue] = useState(value || '')
const [rows, setRows] = useState(1)
const inputRef = useRef<HTMLTextAreaElement>(null)
const [value, setValue] = useState(defaultValue || '')
const [displayValue, setDisplayValue] = useState(defaultValue || '')
const divRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)

const TEXTAREA_WIDTH = 450
const calculateRows = useCalculateRows(TEXTAREA_WIDTH)

useEffect(() => {
if (customEditing) {
setIsEditing(true)
setTimeout(() => {
inputRef.current?.focus()
resizeTextarea()
divRef.current?.focus()
}, 100)
} else {
setIsEditing(false)
}
}, [customEditing])

useEffect(() => {
setInputValue(value || '')
updateRows(value || '')
}, [value])

const handleEdit = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
e.stopPropagation()
const handleEdit = () => {
setIsEditing(true)
setTimeout(() => {
inputRef.current?.focus()
resizeTextarea()
if (divRef.current) {
divRef.current.focus()
}
}, 100)
}

const handleSave = () => {
setIsEditing(false)
onChange(inputValue)
onChange(value)
}

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSave()
} else if (e.key === 'Escape') {
setIsEditing(false)
setInputValue(value || '')
updateRows(value || '')
} else if (e.key === 'a' && e.metaKey) {
e.preventDefault()
inputRef.current?.select()
setDisplayValue(value || '')
}
}

Expand All @@ -124,59 +75,40 @@ export const InlineInput = ({
}
}, [isEditing])

const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(e.target.value)
updateRows(e.target.value)
resizeTextarea()
}

const resizeTextarea = () => {
if (inputRef.current) {
inputRef.current.style.height = 'auto'
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`
}
}

const updateRows = (text: string) => {
const calculatedRows = calculateRows(text)
setRows(calculatedRows > 1 ? calculatedRows : 1)
const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
setValue(e.currentTarget.textContent || '')
}

useEffect(() => {
resizeTextarea()
}, [inputValue])

return (
<div ref={containerRef} className="inline-block w-full">
{isEditing ? (
<textarea
ref={inputRef}
value={inputValue}
onChange={handleInput}
onBlur={handleSave}
onKeyDown={handleKeyDown}
className={cn(
"cursor-pointer hover:bg-default-100 p-1 mt-[3px] rounded-md transition-all duration-200 w-full inline-flex items-center justify-between group inline-input group-data-[state='open']:bg-gray-100 dark:group-data-[state='open']:bg-zinc-900 text-base outline-0 dark:outline-zinc-700 focus:bg-gray-100 dark:focus:bg-zinc-900 resize-none overflow-hidden",
className
)}
style={{ overflow: 'hidden' }}
rows={rows}
/>
) : (
<span
onClick={handleEdit}
className={cn(
"cursor-pointer hover:bg-default-100 p-1 rounded-md transition-all duration-200 w-full inline-flex items-center justify-between group inline-input group-data-[state='open']:bg-gray-100 dark:group-data-[state='open']:bg-zinc-900 text-base outline-0 dark:outline-zinc-700 focus:bg-gray-100 dark:focus:bg-zinc-900",
!inputValue && placeholder && 'text-default-500',
endContent && 'pr-1.5',
className
)}
<div ref={containerRef} className="inline-block w-full relative">
<div
ref={divRef}
contentEditable={isEditing}
suppressContentEditableWarning={true}
onInput={handleInput}
onBlur={handleSave}
onKeyDown={handleKeyDown}
className={cn(
"cursor-pointer hover:bg-default-100 p-1 mt-[3px] rounded-md transition-all duration-200 w-full inline-flex items-center justify-between group inline-input group-data-[state='open']:bg-gray-100 dark:group-data-[state='open']:bg-zinc-900 text-base outline-0 dark:outline-zinc-700 focus:bg-gray-100 dark:focus:bg-zinc-900",
!displayValue && placeholder && 'text-default-500',
endContent && 'pr-1.5',
className
)}
onClick={handleEdit}
data-placeholder={placeholder}
>
{displayValue}
<div className="flex items-center justify-center group-hover:opacity-100 opacity-0 transition-all duration-100">
{endContent}
</div>
</div>
{!displayValue && !isEditing && placeholder && (
<div
className="absolute top-0 left-0 text-default-500 pointer-events-none"
style={{ padding: '1px', marginTop: '3px' }}
>
{inputValue || placeholder}
<div className="flex items-center justify-center group-hover:opacity-100 opacity-0 transition-all duration-100">
{endContent}
</div>
</span>
{placeholder}
</div>
)}
</div>
)
Expand Down

0 comments on commit 396ae44

Please sign in to comment.