mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-17 07:41:35 +03:00
New feature: Shortcuts
This commit is contained in:
parent
75b6cddb04
commit
0bef245c83
12 changed files with 676 additions and 43 deletions
38
src/app.css
38
src/app.css
|
@ -1011,17 +1011,21 @@ button.carousel-dot:is(.active, [disabled].active) {
|
|||
/* MENU POPUP */
|
||||
|
||||
.szh-menu {
|
||||
padding: 8px 0 !important;
|
||||
padding: 8px 0;
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
background-color: var(--bg-color) !important;
|
||||
border: 1px solid var(--outline-color) !important;
|
||||
background-color: var(--bg-color);
|
||||
border: 1px solid var(--outline-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 3px 6px var(--drop-shadow-color);
|
||||
text-align: left;
|
||||
animation: appear 0.15s ease-in-out;
|
||||
width: 16em;
|
||||
max-width: 90vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
.szh-menu__item--focusable {
|
||||
background-color: transparent;
|
||||
}
|
||||
.szh-menu .szh-menu__item {
|
||||
padding: 8px 16px !important;
|
||||
|
@ -1036,11 +1040,18 @@ button.carousel-dot:is(.active, [disabled].active) {
|
|||
.szh-menu .szh-menu__item a {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
display: flex;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding: 8px 16px !important;
|
||||
margin: -8px -16px !important;
|
||||
gap: 8px;
|
||||
}
|
||||
.szh-menu .szh-menu__item a.is-active {
|
||||
font-weight: bold;
|
||||
}
|
||||
.szh-menu .szh-menu__item .icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.szh-menu
|
||||
.szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) {
|
||||
|
@ -1053,6 +1064,25 @@ button.carousel-dot:is(.active, [disabled].active) {
|
|||
.szh-menu__divider {
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.szh-menu .szh-menu__item .menu-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.szh-menu .szh-menu__item .menu-shortcut {
|
||||
opacity: 0.5;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* GLASS MENU */
|
||||
|
||||
.glass-menu {
|
||||
background-color: var(--bg-blur-color);
|
||||
backdrop-filter: blur(8px) saturate(3);
|
||||
border: 0;
|
||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color);
|
||||
}
|
||||
.glass-menu .szh-menu__item--hover {
|
||||
background-color: var(--button-bg-blur-color);
|
||||
}
|
||||
|
||||
/* DONUT METER */
|
||||
|
||||
|
|
40
src/app.jsx
40
src/app.jsx
|
@ -25,6 +25,8 @@ import Link from './components/link';
|
|||
import Loader from './components/loader';
|
||||
import MediaModal from './components/media-modal';
|
||||
import Modal from './components/modal';
|
||||
import Shortcuts from './components/shortcuts';
|
||||
import ShortcutsSettings from './components/shortcuts-settings';
|
||||
import NotFound from './pages/404';
|
||||
import AccountStatuses from './pages/account-statuses';
|
||||
import Bookmarks from './pages/bookmarks';
|
||||
|
@ -146,25 +148,15 @@ function App() {
|
|||
return () => clearTimeout(timer);
|
||||
};
|
||||
useEffect(focusDeck, [location]);
|
||||
const showModal = useMemo(() => {
|
||||
return (
|
||||
snapStates.showCompose ||
|
||||
snapStates.showSettings ||
|
||||
snapStates.showAccount ||
|
||||
snapStates.showDrafts ||
|
||||
snapStates.showMediaModal
|
||||
);
|
||||
}, [
|
||||
snapStates.showCompose,
|
||||
snapStates.showSettings,
|
||||
snapStates.showAccount,
|
||||
snapStates.showDrafts,
|
||||
snapStates.showMediaModal,
|
||||
]);
|
||||
const showModal =
|
||||
snapStates.showCompose ||
|
||||
snapStates.showSettings ||
|
||||
snapStates.showAccount ||
|
||||
snapStates.showDrafts ||
|
||||
snapStates.showMediaModal ||
|
||||
snapStates.showShortcutsSettings;
|
||||
useEffect(() => {
|
||||
if (!showModal) {
|
||||
focusDeck();
|
||||
}
|
||||
if (!showModal) focusDeck();
|
||||
}, [showModal]);
|
||||
|
||||
// useEffect(() => {
|
||||
|
@ -306,6 +298,7 @@ function App() {
|
|||
</Link>
|
||||
</li>
|
||||
</nav>
|
||||
<Shortcuts />
|
||||
{!!snapStates.showCompose && (
|
||||
<Modal>
|
||||
<Compose
|
||||
|
@ -416,6 +409,17 @@ function App() {
|
|||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{!!snapStates.showShortcutsSettings && (
|
||||
<Modal
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
states.showShortcutsSettings = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ShortcutsSettings />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
12
src/components/AsyncText.jsx
Normal file
12
src/components/AsyncText.jsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
function AsyncText({ children }) {
|
||||
if (typeof children === 'string') return children;
|
||||
const [text, setText] = useState('');
|
||||
useEffect(() => {
|
||||
Promise.resolve(children).then(setText);
|
||||
}, [children]);
|
||||
return text;
|
||||
}
|
||||
|
||||
export default AsyncText;
|
21
src/components/MenuLink.jsx
Normal file
21
src/components/MenuLink.jsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { FocusableItem } from '@szhsin/react-menu';
|
||||
|
||||
import Link from './link';
|
||||
|
||||
function MenuLink(props) {
|
||||
return (
|
||||
<FocusableItem>
|
||||
{({ ref, closeMenu }) => (
|
||||
<Link
|
||||
{...props}
|
||||
ref={ref}
|
||||
onClick={({ detail }) =>
|
||||
closeMenu(detail === 0 ? 'Enter' : undefined)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FocusableItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default MenuLink;
|
|
@ -53,6 +53,9 @@ const ICONS = {
|
|||
search: 'mingcute:search-2-line',
|
||||
hashtag: 'mingcute:hashtag-line',
|
||||
info: 'mingcute:information-line',
|
||||
shortcut: 'mingcute:lightning-line',
|
||||
user: 'mingcute:user-4-line',
|
||||
following: 'mingcute:walk-line',
|
||||
};
|
||||
|
||||
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { FocusableItem, Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
import states from '../utils/states';
|
||||
|
||||
import Icon from './icon';
|
||||
import Link from './link';
|
||||
import MenuLink from './MenuLink';
|
||||
|
||||
function NavMenu(props) {
|
||||
const snapStates = useSnapshot(states);
|
||||
|
@ -67,12 +67,20 @@ function NavMenu(props) {
|
|||
{authenticated && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showShortcutsSettings = true;
|
||||
}}
|
||||
>
|
||||
<Icon icon="shortcut" size="l" />{' '}
|
||||
<span>Shortcuts Settings…</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showSettings = true;
|
||||
}}
|
||||
>
|
||||
<Icon icon="gear" size="l" alt="Settings" /> <span>Settings</span>
|
||||
<Icon icon="gear" size="l" /> <span>Settings…</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
@ -80,20 +88,4 @@ function NavMenu(props) {
|
|||
);
|
||||
}
|
||||
|
||||
function MenuLink(props) {
|
||||
return (
|
||||
<FocusableItem>
|
||||
{({ ref, closeMenu }) => (
|
||||
<Link
|
||||
{...props}
|
||||
ref={ref}
|
||||
onClick={({ detail }) =>
|
||||
closeMenu(detail === 0 ? 'Enter' : undefined)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FocusableItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavMenu;
|
||||
|
|
69
src/components/shortcuts-settings.css
Normal file
69
src/components/shortcuts-settings.css
Normal file
|
@ -0,0 +1,69 @@
|
|||
#shortcuts-settings-container .shortcuts-list {
|
||||
line-height: 1.5;
|
||||
padding: 0;
|
||||
margin: 8px 0 0;
|
||||
counter-reset: index;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#shortcuts-settings-container .shortcuts-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
background-color: var(--bg-faded-color);
|
||||
}
|
||||
#shortcuts-settings-container .shortcuts-list li::before {
|
||||
content: counter(index);
|
||||
counter-increment: index;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: right;
|
||||
margin-right: 8px;
|
||||
color: var(--text-insignificant-color);
|
||||
font-size: 90%;
|
||||
}
|
||||
#shortcuts-settings-container .shortcuts-list li .shortcut-text {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#shortcuts-settings-container form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background-color: var(--bg-faded-color);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
#shortcuts-settings-container form header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#shortcuts-settings-container form > * {
|
||||
flex-basis: max(320px, 100%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#shortcuts-settings-container form label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
#shortcuts-settings-container form label > span:first-child {
|
||||
flex-basis: 5em;
|
||||
text-align: right;
|
||||
}
|
||||
#shortcuts-settings-container form :is(input[type='text'], select) {
|
||||
flex-grow: 1;
|
||||
flex-basis: 70%;
|
||||
flex-shrink: 1;
|
||||
/* width: calc(100% - 32px); */
|
||||
min-width: 0;
|
||||
max-width: 320px;
|
||||
}
|
351
src/components/shortcuts-settings.jsx
Normal file
351
src/components/shortcuts-settings.jsx
Normal file
|
@ -0,0 +1,351 @@
|
|||
import './shortcuts-settings.css';
|
||||
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
import states from '../utils/states';
|
||||
|
||||
import AsyncText from './AsyncText';
|
||||
import Icon from './icon';
|
||||
|
||||
const TYPES = [
|
||||
'following',
|
||||
'notifications',
|
||||
'list',
|
||||
'public',
|
||||
'search',
|
||||
// NOTE: Hide for now, can't think of a good way to handle this
|
||||
// 'account-statuses',
|
||||
'bookmarks',
|
||||
'favourites',
|
||||
'hashtag',
|
||||
];
|
||||
const TYPE_TEXT = {
|
||||
following: 'Home',
|
||||
notifications: 'Notifications',
|
||||
list: 'List',
|
||||
public: 'Public',
|
||||
search: 'Search',
|
||||
'account-statuses': 'Account',
|
||||
bookmarks: 'Bookmarks',
|
||||
favourites: 'Favourites',
|
||||
hashtag: 'Hashtag',
|
||||
};
|
||||
const TYPE_PARAMS = {
|
||||
list: [
|
||||
{
|
||||
text: 'List ID',
|
||||
name: 'id',
|
||||
},
|
||||
],
|
||||
public: [
|
||||
{
|
||||
text: 'Local only',
|
||||
name: 'local',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
text: 'Instance',
|
||||
name: 'instance',
|
||||
type: 'text',
|
||||
placeholder: 'e.g. mastodon.social',
|
||||
},
|
||||
],
|
||||
search: [
|
||||
{
|
||||
text: 'Search term',
|
||||
name: 'query',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
'account-statuses': [
|
||||
{
|
||||
text: '@',
|
||||
name: 'id',
|
||||
type: 'text',
|
||||
placeholder: 'cheeaun@mastodon.social',
|
||||
},
|
||||
],
|
||||
hashtag: [
|
||||
{
|
||||
text: '#',
|
||||
name: 'hashtag',
|
||||
type: 'text',
|
||||
placeholder: 'e.g PixelArt',
|
||||
},
|
||||
],
|
||||
};
|
||||
export const SHORTCUTS_META = {
|
||||
following: {
|
||||
title: 'Home',
|
||||
path: (_, index) => (index === 0 ? '/' : '/l/f'),
|
||||
icon: 'home',
|
||||
},
|
||||
notifications: {
|
||||
title: 'Notifications',
|
||||
path: '/notifications',
|
||||
icon: 'notification',
|
||||
},
|
||||
list: {
|
||||
title: async ({ id }) => {
|
||||
const list = await api().masto.v1.lists.fetch(id);
|
||||
return list.title;
|
||||
},
|
||||
path: ({ id }) => `/l/${id}`,
|
||||
icon: 'list',
|
||||
},
|
||||
public: {
|
||||
title: ({ local, instance }) =>
|
||||
`${local ? 'Local' : 'Federated'} (${instance})`,
|
||||
path: ({ local, instance }) => `/${instance}/p${local ? '/l' : ''}`,
|
||||
icon: ({ local }) => (local ? 'group' : 'earth'),
|
||||
},
|
||||
search: {
|
||||
title: ({ query }) => query,
|
||||
path: ({ query }) => `/search?q=${query}`,
|
||||
icon: 'search',
|
||||
},
|
||||
'account-statuses': {
|
||||
title: async ({ id }) => {
|
||||
const account = await api().masto.v1.accounts.fetch(id);
|
||||
return account.username || account.acct || account.displayName;
|
||||
},
|
||||
path: ({ id }) => `/a/${id}`,
|
||||
icon: 'user',
|
||||
},
|
||||
bookmarks: {
|
||||
title: 'Bookmarks',
|
||||
path: '/b',
|
||||
icon: 'bookmark',
|
||||
},
|
||||
favourites: {
|
||||
title: 'Favourites',
|
||||
path: '/f',
|
||||
icon: 'heart',
|
||||
},
|
||||
hashtag: {
|
||||
title: ({ hashtag }) => hashtag,
|
||||
path: ({ hashtag }) => `/t/${hashtag}`,
|
||||
icon: 'hashtag',
|
||||
},
|
||||
};
|
||||
|
||||
function ShortcutsSettings() {
|
||||
const snapStates = useSnapshot(states);
|
||||
const { masto } = api();
|
||||
|
||||
const [lists, setLists] = useState([]);
|
||||
const [followedHashtags, setFollowedHashtags] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const lists = await masto.v1.lists.list();
|
||||
setLists(lists);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const iterator = masto.v1.followedTags.list();
|
||||
const tags = [];
|
||||
do {
|
||||
const { value, done } = await iterator.next();
|
||||
if (done || value?.length === 0) break;
|
||||
tags.push(...value);
|
||||
} while (true);
|
||||
setFollowedHashtags(tags);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="shortcuts-settings-container" class="sheet" tabindex="-1">
|
||||
<header>
|
||||
<h2>
|
||||
<Icon icon="shortcut" /> Shortcuts{' '}
|
||||
<sup
|
||||
style={{
|
||||
fontSize: 12,
|
||||
opacity: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
beta
|
||||
</sup>
|
||||
</h2>
|
||||
</header>
|
||||
<main>
|
||||
<p>
|
||||
Specify a list of shortcuts that'll appear in the floating Shortcuts
|
||||
button.
|
||||
</p>
|
||||
{snapStates.shortcuts.length > 0 ? (
|
||||
<ol class="shortcuts-list">
|
||||
{snapStates.shortcuts.map((shortcut, i) => {
|
||||
const key = i + Object.values(shortcut);
|
||||
const { type } = shortcut;
|
||||
let { icon, title } = SHORTCUTS_META[type];
|
||||
if (typeof title === 'function') {
|
||||
title = title(shortcut);
|
||||
}
|
||||
if (typeof icon === 'function') {
|
||||
icon = icon(shortcut);
|
||||
}
|
||||
return (
|
||||
<li key={key}>
|
||||
<Icon icon={icon} />
|
||||
<span class="shortcut-text">
|
||||
<AsyncText>{title}</AsyncText>
|
||||
</span>
|
||||
<span>
|
||||
<button
|
||||
type="button"
|
||||
class="plain small"
|
||||
disabled={i === 0}
|
||||
onClick={() => {
|
||||
const shortcutsArr = Array.from(states.shortcuts);
|
||||
if (i > 0) {
|
||||
const temp = states.shortcuts[i - 1];
|
||||
shortcutsArr[i - 1] = shortcut;
|
||||
shortcutsArr[i] = temp;
|
||||
states.shortcuts = shortcutsArr;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="arrow-up" alt="Move up" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="plain small"
|
||||
disabled={i === snapStates.shortcuts.length - 1}
|
||||
onClick={() => {
|
||||
const shortcutsArr = Array.from(states.shortcuts);
|
||||
if (i < states.shortcuts.length - 1) {
|
||||
const temp = states.shortcuts[i + 1];
|
||||
shortcutsArr[i + 1] = shortcut;
|
||||
shortcutsArr[i] = temp;
|
||||
states.shortcuts = shortcutsArr;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="arrow-down" alt="Move down" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="plain small"
|
||||
onClick={() => {
|
||||
states.shortcuts.splice(i, 1);
|
||||
}}
|
||||
>
|
||||
<Icon icon="x" alt="Remove" />
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
) : (
|
||||
<p class="ui-state insignificant">
|
||||
No shortcuts yet. Add one from the form below.
|
||||
</p>
|
||||
)}
|
||||
<hr />
|
||||
<ShortcutForm
|
||||
lists={lists}
|
||||
followedHashtags={followedHashtags}
|
||||
onSubmit={(data) => {
|
||||
console.log('onSubmit', data);
|
||||
states.shortcuts.push(data);
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShortcutsSettings;
|
||||
function ShortcutForm({ type, lists, followedHashtags, onSubmit }) {
|
||||
const [currentType, setCurrentType] = useState(type);
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
// Construct a nice object from form
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const result = {};
|
||||
data.forEach((value, key) => {
|
||||
result[key] = value;
|
||||
});
|
||||
if (!result.type) return;
|
||||
onSubmit(result);
|
||||
// Reset
|
||||
e.target.reset();
|
||||
setCurrentType(null);
|
||||
}}
|
||||
>
|
||||
<header>
|
||||
<h3>Add a shortcut</h3>
|
||||
<button type="submit">Add</button>
|
||||
</header>
|
||||
<p>
|
||||
<label>
|
||||
<span>Timeline</span>
|
||||
<select
|
||||
onChange={(e) => {
|
||||
setCurrentType(e.target.value);
|
||||
}}
|
||||
name="type"
|
||||
>
|
||||
<option></option>
|
||||
{TYPES.map((type) => (
|
||||
<option value={type}>{TYPE_TEXT[type]}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
{TYPE_PARAMS[currentType]?.map?.(
|
||||
({ text, name, type, placeholder }) => {
|
||||
if (currentType === 'list') {
|
||||
return (
|
||||
<p>
|
||||
<label>
|
||||
<span>List</span>
|
||||
<select name="id">
|
||||
{lists.map((list) => (
|
||||
<option value={list.id}>{list.title}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
<label>
|
||||
<span>{text}</span>{' '}
|
||||
<input type={type} name={name} placeholder={placeholder} />
|
||||
{currentType === 'hashtag' && followedHashtags.length > 0 && (
|
||||
<datalist>
|
||||
{followedHashtags.map((tag) => (
|
||||
<option value={tag.name} />
|
||||
))}
|
||||
</datalist>
|
||||
)}
|
||||
</label>
|
||||
</p>
|
||||
);
|
||||
},
|
||||
)}
|
||||
<footer></footer>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
39
src/components/shortcuts.css
Normal file
39
src/components/shortcuts.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
#shortcuts-button {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
bottom: max(16px, env(safe-area-inset-bottom));
|
||||
left: 16px;
|
||||
left: max(16px, env(safe-area-inset-left));
|
||||
padding: 16px;
|
||||
background-color: var(--bg-faded-blur-color);
|
||||
z-index: 101;
|
||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color);
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
#shortcuts-button .icon {
|
||||
transform: translateY(2px); /* Balance the icon's vertical alignment */
|
||||
}
|
||||
#app:has(header[hidden]) #shortcuts-button,
|
||||
#shortcuts-button[hidden] {
|
||||
transform: translateY(200%);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
#shortcuts-button:is(:hover, :focus) {
|
||||
background-color: var(--button-color);
|
||||
filter: none;
|
||||
}
|
||||
#shortcuts-button:active {
|
||||
filter: brightness(0.75);
|
||||
}
|
||||
|
||||
@media (min-width: calc(40em + 56px + 8px)) {
|
||||
#shortcuts-button {
|
||||
right: 16px;
|
||||
right: max(16px, env(safe-area-inset-right));
|
||||
left: auto;
|
||||
top: 16px;
|
||||
top: max(16px, env(safe-area-inset-top));
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
103
src/components/shortcuts.jsx
Normal file
103
src/components/shortcuts.jsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import './shortcuts.css';
|
||||
|
||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
||||
import { useRef } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import { SHORTCUTS_META } from '../components/shortcuts-settings';
|
||||
import states from '../utils/states';
|
||||
|
||||
import AsyncText from './AsyncText';
|
||||
import Icon from './icon';
|
||||
import MenuLink from './MenuLink';
|
||||
|
||||
function Shortcuts() {
|
||||
const snapStates = useSnapshot(states);
|
||||
const { shortcuts } = snapStates;
|
||||
|
||||
if (!shortcuts.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const menuRef = useRef();
|
||||
|
||||
const formattedShortcuts = shortcuts.map((pin, i) => {
|
||||
const { type, ...data } = pin;
|
||||
let { path, title, icon } = SHORTCUTS_META[type];
|
||||
|
||||
if (typeof path === 'function') {
|
||||
path = path(data, i);
|
||||
}
|
||||
if (typeof title === 'function') {
|
||||
title = title(data);
|
||||
}
|
||||
if (typeof icon === 'function') {
|
||||
icon = icon(data);
|
||||
}
|
||||
|
||||
return {
|
||||
path,
|
||||
title,
|
||||
icon,
|
||||
};
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
useHotkeys(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (e, handler) => {
|
||||
const index = parseInt(handler.keys[0], 10) - 1;
|
||||
if (index < formattedShortcuts.length) {
|
||||
const { path } = formattedShortcuts[index];
|
||||
if (path) {
|
||||
navigate(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="shortcuts">
|
||||
<Menu
|
||||
instanceRef={menuRef}
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
menuClassName="glass-menu shortcuts-menu"
|
||||
offsetY={4}
|
||||
position="anchor"
|
||||
menuButton={
|
||||
<button
|
||||
type="button"
|
||||
id="shortcuts-button"
|
||||
class="plain"
|
||||
onTransitionStart={(e) => {
|
||||
// Close menu if the button disappears
|
||||
try {
|
||||
const { target } = e;
|
||||
if (getComputedStyle(target).pointerEvents === 'none') {
|
||||
menuRef.current?.closeMenu?.();
|
||||
}
|
||||
} catch (e) {}
|
||||
}}
|
||||
>
|
||||
<Icon icon="shortcut" size="xl" alt="Shortcuts" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{formattedShortcuts.map(({ path, title, icon }, i) => {
|
||||
return (
|
||||
<MenuLink to={path} key={i + title} class="glass-menu-item">
|
||||
<Icon icon={icon} size="l" />{' '}
|
||||
<span class="menu-grow">
|
||||
<AsyncText>{title}</AsyncText>
|
||||
</span>
|
||||
<span class="menu-shortcut">{i + 1}</span>
|
||||
</MenuLink>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Shortcuts;
|
|
@ -62,7 +62,7 @@ function Home() {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="quill" size="xxl" alt="Compose" />
|
||||
<Icon icon="quill" size="xl" alt="Compose" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { proxy } from 'valtio';
|
||||
import { proxy, subscribe } from 'valtio';
|
||||
import { subscribeKey } from 'valtio/utils';
|
||||
|
||||
import { api } from './api';
|
||||
|
@ -30,6 +30,9 @@ const states = proxy({
|
|||
showAccount: false,
|
||||
showDrafts: false,
|
||||
showMediaModal: false,
|
||||
showShortcutsSettings: false,
|
||||
// Shortcuts
|
||||
shortcuts: store.account.get('shortcuts') ?? [],
|
||||
// Settings
|
||||
settings: {
|
||||
boostsCarousel: store.account.get('settings-boostCarousel') ?? true,
|
||||
|
@ -45,6 +48,12 @@ subscribeKey(states, 'notificationsLast', (v) => {
|
|||
subscribeKey(states, 'settings-boostCarousel', (v) => {
|
||||
store.account.set('settings-boostCarousel', !!v);
|
||||
});
|
||||
subscribe(states, (v) => {
|
||||
const [action, path, value] = v[0];
|
||||
if (path?.[0] === 'shortcuts') {
|
||||
store.account.set('shortcuts', states.shortcuts);
|
||||
}
|
||||
});
|
||||
|
||||
export function hideAllModals() {
|
||||
states.showCompose = false;
|
||||
|
|
Loading…
Add table
Reference in a new issue