mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-11-25 10:45:41 +03:00
Experimental opt-in description generator
This commit is contained in:
parent
cfe41cb802
commit
fe54eb11a7
8 changed files with 183 additions and 4 deletions
|
@ -501,6 +501,19 @@
|
||||||
padding-inline: 24px;
|
padding-inline: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes breathe {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#media-sheet {
|
#media-sheet {
|
||||||
.media-form {
|
.media-form {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -514,6 +527,10 @@
|
||||||
resize: none;
|
resize: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* height: 10em; */
|
/* height: 10em; */
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
animation: skeleton-breathe 1.5s linear infinite;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import './compose.css';
|
import './compose.css';
|
||||||
|
|
||||||
import '@github/text-expander-element';
|
import '@github/text-expander-element';
|
||||||
|
import { MenuItem } from '@szhsin/react-menu';
|
||||||
import equal from 'fast-deep-equal';
|
import equal from 'fast-deep-equal';
|
||||||
import { forwardRef } from 'preact/compat';
|
import { forwardRef } from 'preact/compat';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
|
@ -11,6 +12,7 @@ import { uid } from 'uid/single';
|
||||||
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import Menu2 from '../components/menu2';
|
||||||
import supportedLanguages from '../data/status-supported-languages';
|
import supportedLanguages from '../data/status-supported-languages';
|
||||||
import urlRegex from '../data/url-regex';
|
import urlRegex from '../data/url-regex';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
@ -19,6 +21,7 @@ import emojifyText from '../utils/emojify-text';
|
||||||
import localeMatch from '../utils/locale-match';
|
import localeMatch from '../utils/locale-match';
|
||||||
import openCompose from '../utils/open-compose';
|
import openCompose from '../utils/open-compose';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
|
import showToast from '../utils/show-toast';
|
||||||
import states, { saveStatus } from '../utils/states';
|
import states, { saveStatus } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
import {
|
import {
|
||||||
|
@ -39,6 +42,8 @@ import Loader from './loader';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
import Status from './status';
|
import Status from './status';
|
||||||
|
|
||||||
|
const { PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL } = import.meta.env;
|
||||||
|
|
||||||
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
|
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
|
||||||
const [code, common, native] = l;
|
const [code, common, native] = l;
|
||||||
acc[code] = {
|
acc[code] = {
|
||||||
|
@ -1291,7 +1296,7 @@ const Textarea = forwardRef((props, ref) => {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
const [text, setText] = useState(ref.current?.value || '');
|
const [text, setText] = useState(ref.current?.value || '');
|
||||||
const { maxCharacters, performSearch = () => {}, ...textareaProps } = props;
|
const { maxCharacters, performSearch = () => {}, ...textareaProps } = props;
|
||||||
const snapStates = useSnapshot(states);
|
// const snapStates = useSnapshot(states);
|
||||||
// const charCount = snapStates.composerCharacterCount;
|
// const charCount = snapStates.composerCharacterCount;
|
||||||
|
|
||||||
const customEmojis = useRef();
|
const customEmojis = useRef();
|
||||||
|
@ -1645,6 +1650,7 @@ function MediaAttachment({
|
||||||
onDescriptionChange = () => {},
|
onDescriptionChange = () => {},
|
||||||
onRemove = () => {},
|
onRemove = () => {},
|
||||||
}) {
|
}) {
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
const supportsEdit = supports('@mastodon/edit-media-attributes');
|
const supportsEdit = supports('@mastodon/edit-media-attributes');
|
||||||
const { type, id, file } = attachment;
|
const { type, id, file } = attachment;
|
||||||
const url = useMemo(
|
const url = useMemo(
|
||||||
|
@ -1653,7 +1659,7 @@ function MediaAttachment({
|
||||||
);
|
);
|
||||||
console.log({ attachment });
|
console.log({ attachment });
|
||||||
const [description, setDescription] = useState(attachment.description);
|
const [description, setDescription] = useState(attachment.description);
|
||||||
const suffixType = type.split('/')[0];
|
const [suffixType, subtype] = type.split('/');
|
||||||
const debouncedOnDescriptionChange = useDebouncedCallback(
|
const debouncedOnDescriptionChange = useDebouncedCallback(
|
||||||
onDescriptionChange,
|
onDescriptionChange,
|
||||||
250,
|
250,
|
||||||
|
@ -1699,7 +1705,8 @@ function MediaAttachment({
|
||||||
autoCorrect="on"
|
autoCorrect="on"
|
||||||
spellCheck="true"
|
spellCheck="true"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
disabled={disabled}
|
disabled={disabled || uiState === 'loading'}
|
||||||
|
class={uiState === 'loading' ? 'loading' : ''}
|
||||||
maxlength="1500" // Not unicode-aware :(
|
maxlength="1500" // Not unicode-aware :(
|
||||||
// TODO: Un-hard-code this maxlength, ref: https://github.com/mastodon/mastodon/blob/b59fb28e90bc21d6fd1a6bafd13cfbd81ab5be54/app/models/media_attachment.rb#L39
|
// TODO: Un-hard-code this maxlength, ref: https://github.com/mastodon/mastodon/blob/b59fb28e90bc21d6fd1a6bafd13cfbd81ab5be54/app/models/media_attachment.rb#L39
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
|
@ -1712,6 +1719,13 @@ function MediaAttachment({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toastRef = useRef(null);
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
toastRef.current?.hideToast?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="media-attachment">
|
<div class="media-attachment">
|
||||||
|
@ -1785,12 +1799,68 @@ function MediaAttachment({
|
||||||
<div class="media-form">
|
<div class="media-form">
|
||||||
{descTextarea}
|
{descTextarea}
|
||||||
<footer>
|
<footer>
|
||||||
|
{suffixType === 'image' &&
|
||||||
|
/^(png|jpe?g|gif|webp)$/i.test(subtype) &&
|
||||||
|
!!states.settings.mediaAltGenerator &&
|
||||||
|
!!IMG_ALT_API_URL && (
|
||||||
|
<Menu2
|
||||||
|
portal={{
|
||||||
|
target: document.body,
|
||||||
|
}}
|
||||||
|
containerProps={{
|
||||||
|
style: {
|
||||||
|
zIndex: 1001,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
align="center"
|
||||||
|
position="anchor"
|
||||||
|
overflow="auto"
|
||||||
|
menuButton={
|
||||||
|
<button type="button" title="More" class="plain">
|
||||||
|
<Icon icon="more" size="l" alt="More" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
setUIState('loading');
|
||||||
|
toastRef.current = showToast({
|
||||||
|
text: 'Generating description. Please wait...',
|
||||||
|
duration: -1,
|
||||||
|
});
|
||||||
|
// POST with multipart
|
||||||
|
(async function () {
|
||||||
|
try {
|
||||||
|
const body = new FormData();
|
||||||
|
body.append('image', file);
|
||||||
|
const response = await fetch(IMG_ALT_API_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
}).then((r) => r.json());
|
||||||
|
setDescription(response.description);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showToast('Failed to generate description');
|
||||||
|
} finally {
|
||||||
|
setUIState('default');
|
||||||
|
toastRef.current?.hideToast?.();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="sparkles2" />
|
||||||
|
<span>Generate description…</span>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu2>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="light block"
|
class="light block"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
}}
|
}}
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
>
|
>
|
||||||
Done
|
Done
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -69,6 +69,7 @@ export const ICONS = {
|
||||||
history: () => import('@iconify-icons/mingcute/history-line'),
|
history: () => import('@iconify-icons/mingcute/history-line'),
|
||||||
share: () => import('@iconify-icons/mingcute/share-2-line'),
|
share: () => import('@iconify-icons/mingcute/share-2-line'),
|
||||||
sparkles: () => import('@iconify-icons/mingcute/sparkles-line'),
|
sparkles: () => import('@iconify-icons/mingcute/sparkles-line'),
|
||||||
|
sparkles2: () => import('@iconify-icons/mingcute/sparkles-2-line'),
|
||||||
exit: () => import('@iconify-icons/mingcute/exit-line'),
|
exit: () => import('@iconify-icons/mingcute/exit-line'),
|
||||||
translate: () => import('@iconify-icons/mingcute/translate-line'),
|
translate: () => import('@iconify-icons/mingcute/translate-line'),
|
||||||
play: () => import('@iconify-icons/mingcute/play-fill'),
|
play: () => import('@iconify-icons/mingcute/play-fill'),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Menu } from '@szhsin/react-menu';
|
import { MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
|
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
|
||||||
|
import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
@ -18,6 +19,8 @@ import Media from './media';
|
||||||
import Menu2 from './menu2';
|
import Menu2 from './menu2';
|
||||||
import MenuLink from './menu-link';
|
import MenuLink from './menu-link';
|
||||||
|
|
||||||
|
const { PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL } = import.meta.env;
|
||||||
|
|
||||||
function MediaModal({
|
function MediaModal({
|
||||||
mediaAttachments,
|
mediaAttachments,
|
||||||
statusID,
|
statusID,
|
||||||
|
@ -26,6 +29,7 @@ function MediaModal({
|
||||||
index = 0,
|
index = 0,
|
||||||
onClose = () => {},
|
onClose = () => {},
|
||||||
}) {
|
}) {
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
const carouselRef = useRef(null);
|
const carouselRef = useRef(null);
|
||||||
|
|
||||||
const [currentIndex, setCurrentIndex] = useState(index);
|
const [currentIndex, setCurrentIndex] = useState(index);
|
||||||
|
@ -144,6 +148,13 @@ function MediaModal({
|
||||||
);
|
);
|
||||||
}, [mediaAccentColors]);
|
}, [mediaAccentColors]);
|
||||||
|
|
||||||
|
let toastRef = useRef(null);
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
toastRef.current?.hideToast?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`media-modal-container media-modal-count-${mediaAttachments?.length}`}
|
class={`media-modal-container media-modal-count-${mediaAttachments?.length}`}
|
||||||
|
@ -284,6 +295,47 @@ function MediaModal({
|
||||||
<Icon icon="popout" />
|
<Icon icon="popout" />
|
||||||
<span>Open original media</span>
|
<span>Open original media</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
|
{import.meta.env.DEV && // Only dev for now
|
||||||
|
!!states.settings.mediaAltGenerator &&
|
||||||
|
!!IMG_ALT_API_URL &&
|
||||||
|
!!mediaAttachments[currentIndex]?.url &&
|
||||||
|
!mediaAttachments[currentIndex]?.description &&
|
||||||
|
mediaAttachments[currentIndex]?.type === 'image' && (
|
||||||
|
<>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
setUIState('loading');
|
||||||
|
toastRef.current = showToast({
|
||||||
|
text: 'Attempting to describe image. Please wait...',
|
||||||
|
duration: -1,
|
||||||
|
});
|
||||||
|
(async function () {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${IMG_ALT_API_URL}?image=${encodeURIComponent(
|
||||||
|
mediaAttachments[currentIndex]?.url,
|
||||||
|
)}`,
|
||||||
|
).then((r) => r.json());
|
||||||
|
states.showMediaAlt = {
|
||||||
|
alt: response.description,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showToast('Failed to describe image');
|
||||||
|
} finally {
|
||||||
|
setUIState('default');
|
||||||
|
toastRef.current?.hideToast?.();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="sparkles2" />
|
||||||
|
<span>Describe image…</span>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Menu2>{' '}
|
</Menu2>{' '}
|
||||||
<Link
|
<Link
|
||||||
to={`${instance ? `/${instance}` : ''}/s/${statusID}${
|
to={`${instance ? `/${instance}` : ''}/s/${statusID}${
|
||||||
|
|
|
@ -338,6 +338,9 @@ button.large {
|
||||||
font-size: 125%;
|
font-size: 125%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
textarea:disabled {
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
}
|
||||||
|
|
||||||
:is(input[type='text'], textarea, select).block {
|
:is(input[type='text'], textarea, select).block {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -27,6 +27,7 @@ const TEXT_SIZES = [15, 16, 17, 18, 19, 20];
|
||||||
const {
|
const {
|
||||||
PHANPY_WEBSITE: WEBSITE,
|
PHANPY_WEBSITE: WEBSITE,
|
||||||
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
|
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
|
||||||
|
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
|
||||||
} = import.meta.env;
|
} = import.meta.env;
|
||||||
|
|
||||||
function Settings({ onClose }) {
|
function Settings({ onClose }) {
|
||||||
|
@ -432,6 +433,34 @@ function Settings({ onClose }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
{!!IMG_ALT_API_URL && (
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={snapStates.settings.mediaAltGenerator}
|
||||||
|
onChange={(e) => {
|
||||||
|
states.settings.mediaAltGenerator = e.target.checked;
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Image description generator{' '}
|
||||||
|
<Icon icon="sparkles2" class="more-insignificant" />
|
||||||
|
</label>
|
||||||
|
<div class="sub-section insignificant">
|
||||||
|
<small>
|
||||||
|
Note: This feature uses external AI service, powered by{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/cheeaun/img-alt-api"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
img-alt-api
|
||||||
|
</a>
|
||||||
|
. May not work well. Only for images and in English.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
<li>
|
<li>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -23,6 +23,7 @@ function showToast(props) {
|
||||||
} else {
|
} else {
|
||||||
toast.showToast();
|
toast.showToast();
|
||||||
}
|
}
|
||||||
|
return toast;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default showToast;
|
export default showToast;
|
||||||
|
|
|
@ -59,6 +59,7 @@ const states = proxy({
|
||||||
contentTranslationTargetLanguage: null,
|
contentTranslationTargetLanguage: null,
|
||||||
contentTranslationHideLanguages: [],
|
contentTranslationHideLanguages: [],
|
||||||
contentTranslationAutoInline: false,
|
contentTranslationAutoInline: false,
|
||||||
|
mediaAltGenerator: false,
|
||||||
cloakMode: false,
|
cloakMode: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -87,6 +88,8 @@ export function initStates() {
|
||||||
store.account.get('settings-contentTranslationHideLanguages') || [];
|
store.account.get('settings-contentTranslationHideLanguages') || [];
|
||||||
states.settings.contentTranslationAutoInline =
|
states.settings.contentTranslationAutoInline =
|
||||||
store.account.get('settings-contentTranslationAutoInline') ?? false;
|
store.account.get('settings-contentTranslationAutoInline') ?? false;
|
||||||
|
states.settings.mediaAltGenerator =
|
||||||
|
store.account.get('settings-mediaAltGenerator') ?? false;
|
||||||
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
|
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +125,9 @@ subscribe(states, (changes) => {
|
||||||
states.settings.contentTranslationHideLanguages,
|
states.settings.contentTranslationHideLanguages,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (path.join('.') === 'settings.mediaAltGenerator') {
|
||||||
|
store.account.set('settings-mediaAltGenerator', !!value);
|
||||||
|
}
|
||||||
if (path?.[0] === 'shortcuts') {
|
if (path?.[0] === 'shortcuts') {
|
||||||
store.account.set('shortcuts', states.shortcuts);
|
store.account.set('shortcuts', states.shortcuts);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue