fix depecrated deps

This commit is contained in:
SrGooglo 2025-02-11 16:13:13 +00:00
parent 79f64eaec2
commit 3cf055c72c
24 changed files with 2708 additions and 2638 deletions

View File

@ -14,4 +14,5 @@ export default {
"@classes": path.join(__dirname, "src/classes"), "@classes": path.join(__dirname, "src/classes"),
"@models": path.join(__dirname, "../../", "comty.js/src/models"), "@models": path.join(__dirname, "../../", "comty.js/src/models"),
"comty.js": path.join(__dirname, "../../", "comty.js", "src"), "comty.js": path.join(__dirname, "../../", "comty.js", "src"),
"@ragestudio/vessel": path.join(__dirname, "../../", "vessel", "src"),
} }

View File

@ -3,6 +3,7 @@
"version": "1.25.0-a", "version": "1.25.0-a",
"license": "ComtyLicense", "license": "ComtyLicense",
"main": "electron/main", "main": "electron/main",
"type": "module",
"author": "RageStudio", "author": "RageStudio",
"description": "A prototype of a social network.", "description": "A prototype of a social network.",
"scripts": { "scripts": {
@ -13,78 +14,61 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.4.0", "@ant-design/icons": "^5.4.0",
"@capacitor/android": "^5.0.5",
"@capacitor/app": "^5.0.3",
"@capacitor/assets": "^2.0.4",
"@capacitor/cli": "^5.0.5",
"@capacitor/core": "^5.0.5",
"@capacitor/haptics": "1.1.4",
"@capacitor/splash-screen": "^5.0.4",
"@capacitor/status-bar": "^5.0.4",
"@capacitor/storage": "^1.2.5",
"@capgo/capacitor-updater": "^5.0.1",
"@dnd-kit/core": "^6.0.8", "@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2", "@dnd-kit/sortable": "^7.0.2",
"@emotion/react": "^11.13.0", "@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",
"@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.10",
"@ffmpeg/util": "^0.12.1", "@ffmpeg/util": "^0.12.1",
"@loadable/component": "5.15.2",
"@mui/material": "^5.11.9", "@mui/material": "^5.11.9",
"@ragestudio/cordova-nfc": "^1.2.0", "@ragestudio/cordova-nfc": "^1.2.0",
"@ragestudio/vessel": "^0.18.1", "@ragestudio/vessel": "^0.19.0",
"@sentry/browser": "^7.64.0", "@sentry/browser": "^7.64.0",
"@tauri-apps/api": "^1.5.4", "@tauri-apps/api": "^1.5.4",
"@tsmx/human-readable": "^1.0.7", "@tsmx/human-readable": "^1.0.7",
"antd": "^5.20.6", "antd": "^5.20.6",
"axios": "^1.7.7", "axios": "^1.7.7",
"bear-react-carousel": "^4.0.10-alpha.0", "bear-react-carousel": "^4.0.10-alpha.0",
"capacitor-music-controls-plugin-v3": "^1.1.0",
"classnames": "2.3.1", "classnames": "2.3.1",
"dashjs": "^4.7.4", "dashjs": "^4.7.4",
"dompurify": "^3.0.0", "dompurify": "^3.0.0",
"fast-average-color": "^9.2.0", "fast-average-color": "^9.2.0",
"framer-motion": "^10.12.17",
"fuse.js": "6.5.3", "fuse.js": "6.5.3",
"hls.js": "^1.5.17", "hls.js": "^1.5.17",
"howler": "2.2.3", "howler": "2.2.3",
"i18next": "21.6.6", "i18next": "21.6.6",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"jsmediatags": "^3.9.7", "jsmediatags": "^3.9.7",
"less": "4.1.2",
"lottie-react": "^2.4.0", "lottie-react": "^2.4.0",
"luxon": "^3.0.4", "luxon": "^3.0.4",
"million": "^2.6.4",
"mime": "^3.0.0", "mime": "^3.0.0",
"moment": "2.29.4", "moment": "2.29.4",
"motion": "^12.4.2",
"mpegts.js": "^1.6.10", "mpegts.js": "^1.6.10",
"nprogress": "^0.2.0", "plyr": "^3.7.8",
"plyr": "^3.6.12",
"plyr-react": "^3.2.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.2.0", "react": "18.3.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-color": "2.19.3", "react-color": "2.19.3",
"react-countup": "^6.4.1", "react-countup": "^6.4.1",
"react-dom": "18.2.0", "react-dom": "18.3.1",
"react-fast-marquee": "^1.3.5", "react-fast-marquee": "^1.3.5",
"react-helmet": "6.1.0",
"react-i18next": "11.15.3", "react-i18next": "11.15.3",
"react-icons": "^4.8.0", "react-icons": "^5.4.0",
"react-lazy-load-image-component": "^1.5.4", "react-lazy-load-image-component": "^1.5.4",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-modal-image": "^2.6.0", "react-modal-image": "^2.6.0",
"react-motion": "0.5.2", "react-player": "^2.16.0",
"react-rnd": "10.3.5", "react-rnd": "^10.4.14",
"react-router-dom": "^6.26.2", "react-router-dom": "^6.26.2",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-useanimations": "^2.10.0", "react-useanimations": "^2.10.0",
"realtime-bpm-analyzer": "^3.2.1",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
"store": "^2.0.12", "store": "^2.0.12",
"swapy": "^1.0.5",
"ua-parser-js": "^1.0.36", "ua-parser-js": "^1.0.36",
"vaul": "^0.9.2", "vaul": "^1.1.2",
"vite": "^5.4.4" "vite": "^5.4.4"
} }
} }

View File

@ -2,6 +2,7 @@ import "./patches"
import config from "@config" import config from "@config"
import React from "react" import React from "react"
import { Runtime } from "@ragestudio/vessel" import { Runtime } from "@ragestudio/vessel"
import { Helmet } from "react-helmet" import { Helmet } from "react-helmet"
import { Translation } from "react-i18next" import { Translation } from "react-i18next"
@ -10,10 +11,6 @@ import { invoke } from "@tauri-apps/api/tauri"
import { Lightbox } from "react-modal-image" import { Lightbox } from "react-modal-image"
import * as antd from "antd" import * as antd from "antd"
import { StatusBar, Style } from "@capacitor/status-bar"
import { App as CapacitorApp } from "@capacitor/app"
import { CapacitorUpdater } from "@capgo/capacitor-updater"
import AppsMenu from "@components/AppMenu" import AppsMenu from "@components/AppMenu"
import AuthModel from "@models/auth" import AuthModel from "@models/auth"
@ -41,9 +38,9 @@ import Splash from "./splash"
import "@styles/index.less" import "@styles/index.less"
if (IS_MOBILE_HOST) { // if (IS_MOBILE_HOST) {
CapacitorUpdater.notifyAppReady() // CapacitorUpdater.notifyAppReady()
} // }
class ComtyApp extends React.Component { class ComtyApp extends React.Component {
constructor(props) { constructor(props) {
@ -66,8 +63,12 @@ class ComtyApp extends React.Component {
window.app.message = antd.message window.app.message = antd.message
window.app.isCapacitor = IS_MOBILE_HOST window.app.isCapacitor = IS_MOBILE_HOST
if (window.app.version !== window.localStorage.getItem("last_version")) { if (
app.message.info(`Comty has been updated to version ${window.app.version}!`) window.app.version !== window.localStorage.getItem("last_version")
) {
app.message.info(
`Comty has been updated to version ${window.app.version}!`,
)
window.localStorage.setItem("last_version", window.app.version) window.localStorage.setItem("last_version", window.app.version)
} }
@ -113,7 +114,7 @@ class ComtyApp extends React.Component {
sessionController: this.sessionController, sessionController: this.sessionController,
onDone: () => { onDone: () => {
app.layout.draggable.destroy("login") app.layout.draggable.destroy("login")
} },
}, },
}) })
}, },
@ -129,19 +130,23 @@ class ComtyApp extends React.Component {
props: { props: {
bodyStyle: { bodyStyle: {
height: "100%", height: "100%",
} },
}, },
}) })
}, },
// Opens the notification window and sets up the UI for the notification to be displayed // Opens the notification window and sets up the UI for the notification to be displayed
openNotifications: () => { openNotifications: () => {
window.app.layout.drawer.open("notifications", NotificationsCenter, { window.app.layout.drawer.open(
"notifications",
NotificationsCenter,
{
props: { props: {
width: "fit-content", width: "fit-content",
}, },
allowMultiples: false, allowMultiples: false,
escClosable: true, escClosable: true,
}) },
)
}, },
openSearcher: (options) => { openSearcher: (options) => {
if (app.isMobile) { if (app.isMobile) {
@ -150,34 +155,44 @@ class ComtyApp extends React.Component {
componentProps: { componentProps: {
renderResults: true, renderResults: true,
autoFocus: true, autoFocus: true,
} },
}) })
} }
return app.layout.modal.open("searcher", (props) => <Searcher autoFocus renderResults {...props} />, { return app.layout.modal.open(
framed: false "searcher",
}) (props) => <Searcher autoFocus renderResults {...props} />,
{
framed: false,
},
)
}, },
openMessages: () => { openMessages: () => {
app.location.push("/messages") app.location.push("/messages")
}, },
openFullImageViewer: (src) => { openFullImageViewer: (src) => {
app.cores.window_mng.render("image_lightbox", <Lightbox app.cores.window_mng.render(
"image_lightbox",
<Lightbox
small={src} small={src}
large={src} large={src}
onClose={() => app.cores.window_mng.close("image_lightbox")} onClose={() =>
app.cores.window_mng.close("image_lightbox")
}
hideDownload hideDownload
showRotate showRotate
/>) />,
)
}, },
openPostCreator: (params) => { openPostCreator: (params) => {
app.layout.modal.open("post_creator", (props) => <PostCreator app.layout.modal.open(
{...props} "post_creator",
{...params} (props) => <PostCreator {...props} {...params} />,
/>, { {
framed: false framed: false,
}) },
} )
},
}, },
navigation: { navigation: {
reload: () => { reload: () => {
@ -198,14 +213,16 @@ class ComtyApp extends React.Component {
goToSettings: (setting_id) => { goToSettings: (setting_id) => {
return app.location.push(`/settings`, { return app.location.push(`/settings`, {
query: { query: {
setting: setting_id setting: setting_id,
} },
}) })
}, },
goToAccount: (username) => { goToAccount: (username) => {
if (!username) { if (!username) {
if (!app.userData) { if (!app.userData) {
console.error("Cannot go to account, no username provided and no user logged in") console.error(
"Cannot go to account, no username provided and no user logged in",
)
return false return false
} }
@ -219,27 +236,33 @@ class ComtyApp extends React.Component {
}, },
goToPlaylist: (playlist_id) => { goToPlaylist: (playlist_id) => {
return app.location.push(`/play/${playlist_id}`) return app.location.push(`/play/${playlist_id}`)
} },
}, },
capacitor: { capacitor: {
isAppCapacitor: () => window.navigator.userAgent === "capacitor", isAppCapacitor: () => window.navigator.userAgent === "capacitor",
setStatusBarStyleDark: async () => { setStatusBarStyleDark: async () => {
if (!window.app.capacitor.isAppCapacitor()) { if (!window.app.capacitor.isAppCapacitor()) {
console.warn("[App] setStatusBarStyleDark is only available on capacitor") console.warn(
"[App] setStatusBarStyleDark is only available on capacitor",
)
return false return false
} }
return await StatusBar.setStyle({ style: Style.Dark }) return await StatusBar.setStyle({ style: Style.Dark })
}, },
setStatusBarStyleLight: async () => { setStatusBarStyleLight: async () => {
if (!window.app.capacitor.isAppCapacitor()) { if (!window.app.capacitor.isAppCapacitor()) {
console.warn("[App] setStatusBarStyleLight is not supported on this platform") console.warn(
"[App] setStatusBarStyleLight is not supported on this platform",
)
return false return false
} }
return await StatusBar.setStyle({ style: Style.Light }) return await StatusBar.setStyle({ style: Style.Light })
}, },
hideStatusBar: async () => { hideStatusBar: async () => {
if (!window.app.capacitor.isAppCapacitor()) { if (!window.app.capacitor.isAppCapacitor()) {
console.warn("[App] hideStatusBar is not supported on this platform") console.warn(
"[App] hideStatusBar is not supported on this platform",
)
return false return false
} }
@ -247,7 +270,9 @@ class ComtyApp extends React.Component {
}, },
showStatusBar: async () => { showStatusBar: async () => {
if (!window.app.capacitor.isAppCapacitor()) { if (!window.app.capacitor.isAppCapacitor()) {
console.warn("[App] showStatusBar is not supported on this platform") console.warn(
"[App] showStatusBar is not supported on this platform",
)
return false return false
} }
return await StatusBar.show() return await StatusBar.show()
@ -257,13 +282,14 @@ class ComtyApp extends React.Component {
clearInternalStorage: async () => { clearInternalStorage: async () => {
antd.Modal.confirm({ antd.Modal.confirm({
title: "Clear internal storage", title: "Clear internal storage",
content: "Are you sure you want to clear all internal storage? This will remove all your data from the app, including your session.", content:
"Are you sure you want to clear all internal storage? This will remove all your data from the app, including your session.",
onOk: async () => { onOk: async () => {
Utils.deleteInternalStorage() Utils.deleteInternalStorage()
} },
}) })
}, },
} },
} }
static staticRenders = { static staticRenders = {
@ -309,12 +335,10 @@ class ComtyApp extends React.Component {
app.navigation.goAuth() app.navigation.goAuth()
antd.notification.open({ antd.notification.open({
message: <Translation> message: (
{(t) => t("Invalid Session")} <Translation>{(t) => t("Invalid Session")}</Translation>
</Translation>, ),
description: <Translation> description: <Translation>{(t) => t(error)}</Translation>,
{(t) => t(error)}
</Translation>,
icon: <Icons.MdOutlineAccessTimeFilled />, icon: <Icons.MdOutlineAccessTimeFilled />,
}) })
}, },
@ -339,7 +363,7 @@ class ComtyApp extends React.Component {
"auth:disabled_account": async () => { "auth:disabled_account": async () => {
await SessionModel.removeToken() await SessionModel.removeToken()
app.navigation.goAuth() app.navigation.goAuth()
} },
} }
flushState = async () => { flushState = async () => {
@ -355,13 +379,13 @@ class ComtyApp extends React.Component {
StatusBar.setOverlaysWebView({ overlay: false }) StatusBar.setOverlaysWebView({ overlay: false })
CapacitorApp.addListener("backButton", ({ canGoBack }) => { // CapacitorApp.addListener("backButton", ({ canGoBack }) => {
if (!canGoBack) { // if (!canGoBack) {
CapacitorApp.exitApp() // CapacitorApp.exitApp()
} else { // } else {
app.location.back() // app.location.back()
} // }
}) // })
} }
await this.initialization() await this.initialization()
@ -389,7 +413,10 @@ class ComtyApp extends React.Component {
try { try {
await this.__SessionInit() await this.__SessionInit()
} catch (error) { } catch (error) {
console.error(`[App] Error while initializing session`, error) console.error(
`[App] Error while initializing session`,
error,
)
throw { throw {
cause: "Cannot initialize session", cause: "Cannot initialize session",
@ -436,17 +463,19 @@ class ComtyApp extends React.Component {
} }
render() { render() {
return <React.Fragment> return (
<React.Fragment>
<Helmet> <Helmet>
<title>{config.app.siteName}</title> <title>{config.app.siteName}</title>
<meta name="og:description" content={config.app.siteDescription} /> <meta
name="og:description"
content={config.app.siteDescription}
/>
<meta property="og:title" content={config.app.siteName} /> <meta property="og:title" content={config.app.siteName} />
</Helmet> </Helmet>
<Router.InternalRouter> <Router.InternalRouter>
<ThemeProvider> <ThemeProvider>
{ {window.__TAURI__ && <DesktopTopBar />}
window.__TAURI__ && <DesktopTopBar />
}
<Layout <Layout
user={this.state.user} user={this.state.user}
staticRenders={ComtyApp.staticRenders} staticRenders={ComtyApp.staticRenders}
@ -454,13 +483,12 @@ class ComtyApp extends React.Component {
user: this.state.user, user: this.state.user,
}} }}
> >
{ {this.state.initialized && <Router.PageRender />}
this.state.initialized && <Router.PageRender />
}
</Layout> </Layout>
</ThemeProvider> </ThemeProvider>
</Router.InternalRouter> </Router.InternalRouter>
</React.Fragment> </React.Fragment>
)
} }
} }

View File

@ -1,8 +1,9 @@
import React from "react" import React from "react"
import { motion, AnimatePresence } from "framer-motion" import { motion, AnimatePresence } from "motion/react"
const PageTransition = (props) => { const PageTransition = (props) => {
return <AnimatePresence> return (
<AnimatePresence>
<motion.div <motion.div
layout layout
initial={{ y: 10, opacity: 0 }} initial={{ y: 10, opacity: 0 }}
@ -14,6 +15,7 @@ const PageTransition = (props) => {
{props.children} {props.children}
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
)
} }
export default PageTransition export default PageTransition

View File

@ -2,7 +2,7 @@ import React from "react"
import * as antd from "antd" import * as antd from "antd"
import classnames from "classnames" import classnames from "classnames"
import { Icons, createIconRender } from "@components/Icons" import { Icons, createIconRender } from "@components/Icons"
import { motion } from "framer-motion" import { motion } from "motion/react"
import useWsEvents from "@hooks/useWsEvents" import useWsEvents from "@hooks/useWsEvents"
@ -17,44 +17,36 @@ const PollOption = (props) => {
} }
} }
return <div return (
className={classnames( <div
"poll-option", className={classnames("poll-option", {
{
["checked"]: props.checked, ["checked"]: props.checked,
})}
}
)}
style={{ style={{
"--percentage": `${props.percentage}%` "--percentage": `${props.percentage}%`,
}} }}
onClick={onClick} onClick={onClick}
> >
{ {props.checked && (
props.checked && <motion.div <motion.div
className="percentage-indicator" className="percentage-indicator"
animate={{ width: `${props.percentage}%` }} animate={{ width: `${props.percentage}%` }}
initial={{ width: 0 }} initial={{ width: 0 }}
transition={{ ease: "easeOut" }} transition={{ ease: "easeOut" }}
/> />
} )}
<div className="poll-option-content"> <div className="poll-option-content">
{ {props.checked && createIconRender("FaCheck")}
props.checked && createIconRender("FaCheck")
}
{ {props.showPercentage && (
props.showPercentage && <span> <span>{Math.floor(props.percentage)}%</span>
{Math.floor(props.percentage)}% )}
</span>
}
<span> <span>{props.option.label}</span>
{props.option.label}
</span>
</div> </div>
</div> </div>
)
} }
const Poll = (props) => { const Poll = (props) => {
@ -64,7 +56,8 @@ const Poll = (props) => {
const [hasVoted, setHasVoted] = React.useState(false) const [hasVoted, setHasVoted] = React.useState(false)
const [totalVotes, setTotalVotes] = React.useState(0) const [totalVotes, setTotalVotes] = React.useState(0)
useWsEvents({ useWsEvents(
{
"post.poll.vote": (data) => { "post.poll.vote": (data) => {
const { post_id, option_id, user_id, previous_option_id } = data const { post_id, option_id, user_id, previous_option_id } = data
@ -91,15 +84,20 @@ const Poll = (props) => {
} }
if (previous_option_id) { if (previous_option_id) {
const previousOptionIndex = prev.findIndex((option) => option.id === previous_option_id) const previousOptionIndex = prev.findIndex(
(option) => option.id === previous_option_id,
)
if (previousOptionIndex !== -1) { if (previousOptionIndex !== -1) {
prev[previousOptionIndex].count = prev[previousOptionIndex].count - 1 prev[previousOptionIndex].count =
prev[previousOptionIndex].count - 1
} }
} }
if (option_id) { if (option_id) {
const newOptionIndex = prev.findIndex((option) => option.id === option_id) const newOptionIndex = prev.findIndex(
(option) => option.id === option_id,
)
if (newOptionIndex !== -1) { if (newOptionIndex !== -1) {
prev[newOptionIndex].count += 1 prev[newOptionIndex].count += 1
@ -108,10 +106,12 @@ const Poll = (props) => {
return prev return prev
}) })
} },
}, { },
socketName: "posts" {
}) socketName: "posts",
},
)
async function onVote(id) { async function onVote(id) {
console.debug(`Voting poll option`, { console.debug(`Voting poll option`, {
@ -141,12 +141,15 @@ const Poll = (props) => {
} }
}, [options]) }, [options])
return <div className="poll"> return (
{ <div className="poll">
!editMode && options.map((option, index) => { {!editMode &&
const percentage = totalVotes > 0 ? (option.count / totalVotes) * 100 : 0 options.map((option, index) => {
const percentage =
totalVotes > 0 ? (option.count / totalVotes) * 100 : 0
return <PollOption return (
<PollOption
key={index} key={index}
option={option} option={option}
onClick={onVote} onClick={onVote}
@ -154,26 +157,25 @@ const Poll = (props) => {
percentage={percentage} percentage={percentage}
showPercentage={hasVoted} showPercentage={hasVoted}
/> />
}) )
} })}
{ {editMode && (
editMode && <antd.Form <antd.Form
name="post-poll" name="post-poll"
className="post-poll-edit" className="post-poll-edit"
ref={formRef} ref={formRef}
initialValues={{ initialValues={{
options: options options: options,
}} }}
> >
<antd.Form.List <antd.Form.List name="options">
name="options"
>
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
return <> return (
{ <>
fields.map((field, index) => { {fields.map((field, index) => {
return <div return (
<div
key={field.key} key={field.key}
className="post-poll-edit-option" className="post-poll-edit-option"
> >
@ -181,20 +183,22 @@ const Poll = (props) => {
{...field} {...field}
name={[field.name, "label"]} name={[field.name, "label"]}
> >
<antd.Input <antd.Input placeholder="Type a option" />
placeholder="Type a option"
/>
</antd.Form.Item> </antd.Form.Item>
{ {fields.length > 1 && (
fields.length > 1 && <antd.Button <antd.Button
onClick={() => remove(field.name)} onClick={() =>
icon={createIconRender("MdRemove")} remove(field.name)
}
icon={createIconRender(
"MdRemove",
)}
/> />
} )}
</div> </div>
}) )
} })}
<antd.Button <antd.Button
onClick={() => add()} onClick={() => add()}
@ -203,13 +207,14 @@ const Poll = (props) => {
Add Option Add Option
</antd.Button> </antd.Button>
</> </>
)
}} }}
</antd.Form.List> </antd.Form.List>
</antd.Form> </antd.Form>
} )}
{ {editMode && (
editMode && <div className="poll-edit-actions"> <div className="poll-edit-actions">
<antd.Button <antd.Button
onClick={onClose} onClick={onClose}
icon={createIconRender("CloseOutlined")} icon={createIconRender("CloseOutlined")}
@ -217,8 +222,9 @@ const Poll = (props) => {
type="text" type="text"
/> />
</div> </div>
} )}
</div> </div>
)
} }
export default Poll export default Poll

View File

@ -1,6 +1,5 @@
import React from "react" import React from "react"
import { Skeleton, Button } from "antd" import { Skeleton, Button } from "antd"
import Plyr from "plyr-react"
import mimetypes from "mime" import mimetypes from "mime"
import BearCarousel from "bear-react-carousel" import BearCarousel from "bear-react-carousel"
@ -9,7 +8,6 @@ import ImageViewer from "@components/ImageViewer"
import ContentFailed from "../contentFailed" import ContentFailed from "../contentFailed"
import "bear-react-carousel/dist/index.css" import "bear-react-carousel/dist/index.css"
import "plyr-react/dist/plyr.css"
import "./index.less" import "./index.less"
const renderDebug = localStorage.getItem("render_debug") === "true" const renderDebug = localStorage.getItem("render_debug") === "true"
@ -75,19 +73,21 @@ const Attachment = React.memo((props) => {
return <ImageViewer src={url} /> return <ImageViewer src={url} />
} }
case "video": { case "video": {
return <video controls> return (
<video controls>
<source src={url} type={mimeType} /> <source src={url} type={mimeType} />
</video> </video>
)
} }
case "audio": { case "audio": {
return <audio controls> return (
<audio controls>
<source src={url} type={mimeType} /> <source src={url} type={mimeType} />
</audio> </audio>
)
} }
default: { default: {
return <h4> return <h4>Unsupported media type [{mimeType}]</h4>
Unsupported media type [{mimeType}]
</h4>
} }
} }
} }
@ -104,7 +104,8 @@ const Attachment = React.memo((props) => {
return <ContentFailed /> return <ContentFailed />
} }
return <div return (
<div
key={props.index} key={props.index}
id={id} id={id}
className="attachment" className="attachment"
@ -112,6 +113,7 @@ const Attachment = React.memo((props) => {
> >
{renderMedia()} {renderMedia()}
</div> </div>
)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -126,29 +128,29 @@ export default React.memo((props) => {
React.useEffect(() => { React.useEffect(() => {
// get attachment index from query string // get attachment index from query string
const attachmentIndex = parseInt(new URLSearchParams(window.location.search).get("attachment")) const attachmentIndex = parseInt(
new URLSearchParams(window.location.search).get("attachment"),
)
if (attachmentIndex) { if (attachmentIndex) {
controller?.slideToPage(attachmentIndex) controller?.slideToPage(attachmentIndex)
} }
}, []) }, [])
return <div className="post_attachments"> return (
{ <div className="post_attachments">
props.flags && props.flags.includes("nsfw") && !nsfwAccepted && {props.flags && props.flags.includes("nsfw") && !nsfwAccepted && (
<div className="nsfw_alert"> <div className="nsfw_alert">
<h2> <h2>This post may contain sensitive content.</h2>
This post may contain sensitive content.
</h2>
<Button onClick={() => setNsfwAccepted(true)}> <Button onClick={() => setNsfwAccepted(true)}>
Show anyways Show anyways
</Button> </Button>
</div> </div>
} )}
{ {props.attachments?.length > 0 && (
props.attachments?.length > 0 && <BearCarousel <BearCarousel
data={props.attachments.map((attachment, index) => { data={props.attachments.map((attachment, index) => {
if (typeof attachment !== "object") { if (typeof attachment !== "object") {
attachment = { attachment = {
@ -158,9 +160,14 @@ export default React.memo((props) => {
return { return {
key: index, key: index,
children: <React.Fragment key={index}> children: (
<Attachment index={index} attachment={attachment} /> <React.Fragment key={index}>
<Attachment
index={index}
attachment={attachment}
/>
</React.Fragment> </React.Fragment>
),
} }
})} })}
isEnableNavButton isEnableNavButton
@ -170,6 +177,7 @@ export default React.memo((props) => {
onSlideChange={setCarouselState} onSlideChange={setCarouselState}
isDebug={renderDebug} isDebug={renderDebug}
/> />
} )}
</div> </div>
)
}) })

View File

@ -1,7 +1,7 @@
import React from "react" import React from "react"
import classnames from "classnames" import classnames from "classnames"
import Plyr from "plyr-react" import ReactPlayer from "react-player/lazy"
import { motion } from "framer-motion" import { motion } from "motion/react"
import Poll from "@components/Poll" import Poll from "@components/Poll"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
@ -16,42 +16,65 @@ import "./index.less"
const articleAnimationProps = { const articleAnimationProps = {
layout: true, layout: true,
initial: { y: -100, opacity: 0 }, initial: { y: -100, opacity: 0 },
animate: { y: 0, opacity: 1, }, animate: { y: 0, opacity: 1 },
exit: { scale: 0, opacity: 0 }, exit: { scale: 0, opacity: 0 },
transition: { duration: 0.1, }, transition: { duration: 0.1 },
} }
const messageRegexs = [ const messageRegexs = [
{ {
regex: /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})(&[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+)*/g, regex: /https?:\/\/(?:www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})(&[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+)*/g,
fn: (key, result) => { fn: (key, result) => {
return <Plyr source={{ return <ReactPlayer url={result[1]} controls />
type: "video", },
sources: [{
src: result[1],
provider: "youtube",
}],
}} />
}
}, },
{ {
regex: /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi, regex: /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi,
fn: (key, result) => { fn: (key, result) => {
return <a key={key} href={result[1]} target="_blank" rel="noopener noreferrer">{result[1]}</a> return (
} <a
key={key}
href={result[1]}
target="_blank"
rel="noopener noreferrer"
>
{result[1]}
</a>
)
},
}, },
{ {
regex: /(@[a-zA-Z0-9_]+)/gi, regex: /(@[a-zA-Z0-9_]+)/gi,
fn: (key, result) => { fn: (key, result) => {
return <a key={key} onClick={() => window.app.location.push(`/@${result[1].substr(1)}`)}>{result[1]}</a> return (
<a
key={key}
onClick={() =>
window.app.location.push(`/@${result[1].substr(1)}`)
} }
>
{result[1]}
</a>
)
},
}, },
{ {
regex: /#[a-zA-Z0-9_]+/gi, regex: /#[a-zA-Z0-9_]+/gi,
fn: (key, result) => { fn: (key, result) => {
return <a key={key} onClick={() => window.app.location.push(`/trending/${result[0].substr(1)}`)}>{result[0]}</a> return (
} <a
key={key}
onClick={() =>
window.app.location.push(
`/trending/${result[0].substr(1)}`,
)
} }
>
{result[0]}
</a>
)
},
},
] ]
export default class PostCard extends React.PureComponent { export default class PostCard extends React.PureComponent {
@ -101,7 +124,9 @@ export default class PostCard extends React.PureComponent {
return return
} }
const actionResult = await this.props.events.onClickLike(this.state.data) const actionResult = await this.props.events.onClickLike(
this.state.data,
)
if (actionResult) { if (actionResult) {
this.setState({ this.setState({
@ -119,7 +144,9 @@ export default class PostCard extends React.PureComponent {
return return
} }
const actionResult = await this.props.events.onClickSave(this.state.data) const actionResult = await this.props.events.onClickSave(
this.state.data,
)
if (actionResult) { if (actionResult) {
this.setState({ this.setState({
@ -159,33 +186,42 @@ export default class PostCard extends React.PureComponent {
componentDidCatch = (error, info) => { componentDidCatch = (error, info) => {
console.error(error) console.error(error)
return <div className="postCard error"> return (
<div className="postCard error">
<h1> <h1>
<Icons.FiAlertTriangle /> <Icons.FiAlertTriangle />
<span>Cannot render this post</span> <span>Cannot render this post</span>
<span> <span>
Maybe this version of the app is outdated or is not supported yet Maybe this version of the app is outdated or is not
supported yet
</span> </span>
</h1> </h1>
</div> </div>
)
} }
componentDidMount = () => { componentDidMount = () => {
app.cores.api.listenEvent(`post.update.${this.state.data._id}`, this.handleDataUpdate, "posts") app.cores.api.listenEvent(
`post.update.${this.state.data._id}`,
this.handleDataUpdate,
"posts",
)
} }
componentWillUnmount = () => { componentWillUnmount = () => {
app.cores.api.unlistenEvent(`post.update.${this.state.data._id}`, this.handleDataUpdate, "posts") app.cores.api.unlistenEvent(
`post.update.${this.state.data._id}`,
this.handleDataUpdate,
"posts",
)
} }
render() { render() {
return <motion.article return (
className={classnames( <motion.article
"post_card", className={classnames("post_card", {
{
["open"]: this.state.open, ["open"]: this.state.open,
} })}
)}
id={this.state.data._id} id={this.state.data._id}
style={this.props.style} style={this.props.style}
{...articleAnimationProps} {...articleAnimationProps}
@ -203,38 +239,36 @@ export default class PostCard extends React.PureComponent {
<div <div
id="post_content" id="post_content"
className={classnames( className={classnames("post_content")}
"post_content",
)}
> >
<div className="message"> <div className="message">
{ {processString(messageRegexs)(
processString(messageRegexs)(this.state.data.message ?? "") this.state.data.message ?? "",
} )}
</div> </div>
{ {!this.props.disableAttachments &&
!this.props.disableAttachments && this.state.data.attachments && this.state.data.attachments.length > 0 && <PostAttachments this.state.data.attachments &&
this.state.data.attachments.length > 0 && (
<PostAttachments
attachments={this.state.data.attachments} attachments={this.state.data.attachments}
flags={this.state.data.flags} flags={this.state.data.flags}
/> />
} )}
{ {this.state.data.poll_options && (
this.state.data.poll_options && <Poll <Poll
post_id={this.state.data._id} post_id={this.state.data._id}
options={this.state.data.poll_options} options={this.state.data.poll_options}
/> />
} )}
</div> </div>
<PostActions <PostActions
post={this.state.data} post={this.state.data}
user_id={this.state.data.user_id} user_id={this.state.data.user_id}
likesCount={this.state.countLikes} likesCount={this.state.countLikes}
repliesCount={this.state.countReplies} repliesCount={this.state.countReplies}
defaultLiked={this.state.hasLiked} defaultLiked={this.state.hasLiked}
defaultSaved={this.state.hasSaved} defaultSaved={this.state.hasSaved}
PP PP
@ -247,16 +281,21 @@ PP
}} }}
/> />
{ {!this.props.disableHasReplies &&
!this.props.disableHasReplies && !!this.state.hasReplies && <div !!this.state.hasReplies && (
<div
className="post-card-has_replies" className="post-card-has_replies"
onClick={() => app.navigation.goToPost(this.state.data._id)} onClick={() =>
app.navigation.goToPost(this.state.data._id)
}
> >
<span>View {this.state.hasReplies} replies</span> <span>
View {this.state.hasReplies} replies
</span>
</div> </div>
} )}
</div> </div>
</motion.article> </motion.article>
)
} }
} }

View File

@ -1,6 +1,6 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import { AnimatePresence } from "framer-motion" import { AnimatePresence } from "motion/react"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import PostCard from "@components/PostCard" import PostCard from "@components/PostCard"
@ -11,34 +11,40 @@ import PostModel from "@models/post"
import "./index.less" import "./index.less"
const LoadingComponent = () => { const LoadingComponent = () => {
return <div className="post_card"> return (
<div className="post_card">
<antd.Skeleton <antd.Skeleton
avatar avatar
style={{ style={{
width: "100%" width: "100%",
}} }}
/> />
</div> </div>
)
} }
const NoResultComponent = () => { const NoResultComponent = () => {
return <antd.Empty return (
<antd.Empty
description="No more post here" description="No more post here"
style={{ style={{
width: "100%" width: "100%",
}} }}
/> />
)
} }
const typeToComponent = { const typeToComponent = {
"post": (args) => <PostCard {...args} />, post: (args) => <PostCard {...args} />,
//"playlist": (args) => <PlaylistTimelineEntry {...args} />, //"playlist": (args) => <PlaylistTimelineEntry {...args} />,
} }
const Entry = React.memo((props) => { const Entry = React.memo((props) => {
const { data } = props const { data } = props
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, { return React.createElement(
typeToComponent[data.type ?? "post"] ?? PostCard,
{
key: data._id, key: data._id,
data: data, data: data,
disableReplyTag: props.disableReplyTag, disableReplyTag: props.disableReplyTag,
@ -51,11 +57,13 @@ const Entry = React.memo((props) => {
onClickReply: props.onReplyPost, onClickReply: props.onReplyPost,
onDoubleClick: props.onDoubleClick, onDoubleClick: props.onDoubleClick,
}, },
}) },
)
}) })
const PostList = React.forwardRef((props, ref) => { const PostList = React.forwardRef((props, ref) => {
return <LoadMore return (
<LoadMore
ref={ref} ref={ref}
className="post-list" className="post-list"
loadingComponent={LoadingComponent} loadingComponent={LoadingComponent}
@ -64,8 +72,8 @@ const PostList = React.forwardRef((props, ref) => {
fetching={props.loading} fetching={props.loading}
onBottom={props.onLoadMore} onBottom={props.onLoadMore}
> >
{ {!props.realtimeUpdates && !app.isMobile && (
!props.realtimeUpdates && !app.isMobile && <div className="resume_btn_wrapper"> <div className="resume_btn_wrapper">
<antd.Button <antd.Button
type="primary" type="primary"
shape="round" shape="round"
@ -76,21 +84,15 @@ const PostList = React.forwardRef((props, ref) => {
Resume Resume
</antd.Button> </antd.Button>
</div> </div>
} )}
<AnimatePresence> <AnimatePresence>
{ {props.list.map((data) => {
props.list.map((data) => { return <Entry key={data._id} data={data} {...props} />
return <Entry })}
key={data._id}
data={data}
{...props}
/>
})
}
</AnimatePresence> </AnimatePresence>
</LoadMore> </LoadMore>
)
}) })
export class PostsListsComponent extends React.Component { export class PostsListsComponent extends React.Component {
@ -144,7 +146,7 @@ export class PostsListsComponent extends React.Component {
return post._id !== id return post._id !== id
}), }),
}) })
} },
} }
handleLoad = async (fn, params = {}) => { handleLoad = async (fn, params = {}) => {
@ -228,7 +230,7 @@ export class PostsListsComponent extends React.Component {
_id: randomId, _id: randomId,
username: "random user", username: "random user",
avatar: `https://api.dicebear.com/7.x/thumbs/svg?seed=${randomId}`, avatar: `https://api.dicebear.com/7.x/thumbs/svg?seed=${randomId}`,
} },
}) })
}, },
listRef: this.listRef, listRef: this.listRef,
@ -278,7 +280,11 @@ export class PostsListsComponent extends React.Component {
} }
} }
if (!this.props.realtime || this.state.resumingLoading || this.state.scrollingToTop) { if (
!this.props.realtime ||
this.state.resumingLoading ||
this.state.scrollingToTop
) {
return null return null
} }
@ -317,10 +323,16 @@ export class PostsListsComponent extends React.Component {
} else { } else {
this.props.watchTimeline.forEach((event) => { this.props.watchTimeline.forEach((event) => {
if (typeof this.timelineWsEvents[event] !== "function") { if (typeof this.timelineWsEvents[event] !== "function") {
console.error(`The event "${event}" is not defined in the timelineWsEvents object`) console.error(
`The event "${event}" is not defined in the timelineWsEvents object`,
)
} }
app.cores.api.listenEvent(event, this.timelineWsEvents[event], "posts") app.cores.api.listenEvent(
event,
this.timelineWsEvents[event],
"posts",
)
}) })
} }
} }
@ -339,16 +351,25 @@ export class PostsListsComponent extends React.Component {
} else { } else {
this.props.watchTimeline.forEach((event) => { this.props.watchTimeline.forEach((event) => {
if (typeof this.timelineWsEvents[event] !== "function") { if (typeof this.timelineWsEvents[event] !== "function") {
console.error(`The event "${event}" is not defined in the timelineWsEvents object`) console.error(
`The event "${event}" is not defined in the timelineWsEvents object`,
)
} }
app.cores.api.unlistenEvent(event, this.timelineWsEvents[event], "posts") app.cores.api.unlistenEvent(
event,
this.timelineWsEvents[event],
"posts",
)
}) })
} }
} }
if (this.listRef && this.listRef.current) { if (this.listRef && this.listRef.current) {
this.listRef.current.removeEventListener("scroll", this.onScrollList) this.listRef.current.removeEventListener(
"scroll",
this.onScrollList,
)
} }
window._hacks = null window._hacks = null
@ -363,21 +384,25 @@ export class PostsListsComponent extends React.Component {
} }
onLikePost = async (data) => { onLikePost = async (data) => {
let result = await PostModel.toggleLike({ post_id: data._id }).catch(() => { let result = await PostModel.toggleLike({ post_id: data._id }).catch(
() => {
antd.message.error("Failed to like post") antd.message.error("Failed to like post")
return false return false
}) },
)
return result return result
} }
onSavePost = async (data) => { onSavePost = async (data) => {
let result = await PostModel.toggleSave({ post_id: data._id }).catch(() => { let result = await PostModel.toggleSave({ post_id: data._id }).catch(
() => {
antd.message.error("Failed to save post") antd.message.error("Failed to save post")
return false return false
}) },
)
return result return result
} }
@ -437,10 +462,12 @@ export class PostsListsComponent extends React.Component {
return React.createElement(this.props.emptyListRender) return React.createElement(this.props.emptyListRender)
} }
return <div className="no_more_posts"> return (
<div className="no_more_posts">
<antd.Empty /> <antd.Empty />
<h1>Whoa, nothing on here...</h1> <h1>Whoa, nothing on here...</h1>
</div> </div>
)
} }
const PostListProps = { const PostListProps = {
@ -466,19 +493,17 @@ export class PostsListsComponent extends React.Component {
} }
if (app.isMobile) { if (app.isMobile) {
return <PostList return <PostList ref={this.listRef} {...PostListProps} />
ref={this.listRef}
{...PostListProps}
/>
} }
return <div className="post-list_wrapper"> return (
<PostList <div className="post-list_wrapper">
ref={this.listRef} <PostList ref={this.listRef} {...PostListProps} />
{...PostListProps}
/>
</div> </div>
)
} }
} }
export default React.forwardRef((props, ref) => <PostsListsComponent innerRef={ref} {...props} />) export default React.forwardRef((props, ref) => (
<PostsListsComponent innerRef={ref} {...props} />
))

View File

@ -1,7 +1,7 @@
import React from "react" import React from "react"
import { createIconRender } from "@components/Icons" import { createIconRender } from "@components/Icons"
import { AnimatePresence, motion } from "framer-motion" import { AnimatePresence, motion } from "motion/react"
import "./index.less" import "./index.less"
@ -26,9 +26,11 @@ const ContextMenu = (props) => {
const renderItems = () => { const renderItems = () => {
if (items.length === 0) { if (items.length === 0) {
return <div> return (
<div>
<p>No items</p> <p>No items</p>
</div> </div>
)
} }
return items.map((item, index) => { return items.map((item, index) => {
@ -36,31 +38,28 @@ const ContextMenu = (props) => {
return <div key={index} className="context-menu-separator" /> return <div key={index} className="context-menu-separator" />
} }
return <div return (
<div
key={index} key={index}
onClick={() => handleItemClick(item)} onClick={() => handleItemClick(item)}
className="item" className="item"
> >
<p className="label"> <p className="label">{item.label}</p>
{item.label}
</p>
{ {item.description && (
item.description && <p className="description"> <p className="description">{item.description}</p>
{item.description} )}
</p>
}
{ {createIconRender(item.icon)}
createIconRender(item.icon)
}
</div> </div>
)
}) })
} }
return <AnimatePresence> return (
{ <AnimatePresence>
visible && <div {visible && (
<div
className="context-menu-wrapper" className="context-menu-wrapper"
style={{ style={{
top: cords.y, top: cords.y,
@ -74,13 +73,12 @@ const ContextMenu = (props) => {
exit={{ opacity: 0, scale: 0.3 }} exit={{ opacity: 0, scale: 0.3 }}
transition={{ duration: 0.05, ease: "easeInOut" }} transition={{ duration: 0.05, ease: "easeInOut" }}
> >
{ {renderItems()}
renderItems()
}
</motion.div> </motion.div>
</div> </div>
} )}
</AnimatePresence> </AnimatePresence>
)
} }
export default ContextMenu export default ContextMenu

View File

@ -1,5 +1,5 @@
import { Core } from "@ragestudio/vessel" import { Core } from "@ragestudio/vessel"
import { Haptics } from "@capacitor/haptics" // import { Haptics } from "@capacitor/haptics"
const vibrationPatterns = { const vibrationPatterns = {
light: [10], light: [10],
@ -11,9 +11,7 @@ const vibrationPatterns = {
export default class HapticsCore extends Core { export default class HapticsCore extends Core {
static namespace = "haptics" static namespace = "haptics"
static dependencies = [ static dependencies = ["settings"]
"settings"
]
static isGlobalDisabled() { static isGlobalDisabled() {
return app.cores.settings.is("haptics:enabled", false) return app.cores.settings.is("haptics:enabled", false)
@ -28,24 +26,25 @@ export default class HapticsCore extends Core {
} }
public = { public = {
isGlobalDisabled: HapticsCore.isGlobalDisabled, //isGlobalDisabled: HapticsCore.isGlobalDisabled,
vibrate: this.vibrate.bind(this), vibrate: this.vibrate.bind(this),
} }
nativeVibrate = (pattern) => { // nativeVibrate = (pattern) => {
if (!Array.isArray(pattern)) { // if (!Array.isArray(pattern)) {
pattern = [pattern] // pattern = [pattern]
} // }
for (let i = 0; i < pattern.length; i++) { // for (let i = 0; i < pattern.length; i++) {
Haptics.vibrate({ // Haptics.vibrate({
duration: pattern[i], // duration: pattern[i],
}) // })
} // }
} // }
handleClickEvent = (event) => { handleClickEvent = (event) => {
const button = event.target.closest("button") || event.target.closest(".ant-btn") const button =
event.target.closest("button") || event.target.closest(".ant-btn")
if (button) { if (button) {
this.vibrate("light") this.vibrate("light")

View File

@ -1,4 +1,4 @@
import { Haptics } from "@capacitor/haptics" //import { Haptics } from "@capacitor/haptics"
const NotfTypeToAudio = { const NotfTypeToAudio = {
info: "notification", info: "notification",
@ -9,21 +9,28 @@ const NotfTypeToAudio = {
class NotificationFeedback { class NotificationFeedback {
static getSoundVolume = () => { static getSoundVolume = () => {
return (window.app.cores.settings.get("sfx:notifications_volume") ?? 50) / 100 return (
(window.app.cores.settings.get("sfx:notifications_volume") ?? 50) /
100
)
} }
static playHaptic = async () => { static playHaptic = async () => {
if (app.cores.settings.get("haptics:notifications_feedback")) { if (app.cores.settings.get("haptics:notifications_feedback")) {
await Haptics.vibrate() //await Haptics.vibrate()
//use navigator.vibrate
} }
} }
static playAudio = (type) => { static playAudio = (type) => {
if (app.cores.settings.get("sfx:notifications_feedback")) { if (app.cores.settings.get("sfx:notifications_feedback")) {
if (typeof window.app.cores.sfx?.play === "function") { if (typeof window.app.cores.sfx?.play === "function") {
window.app.cores.sfx.play(NotfTypeToAudio[type] ?? "notification", { window.app.cores.sfx.play(
NotfTypeToAudio[type] ?? "notification",
{
volume: NotificationFeedback.getSoundVolume(), volume: NotificationFeedback.getSoundVolume(),
}) },
)
} }
} }
} }

View File

@ -1,131 +0,0 @@
import { CapacitorMusicControls } from "capacitor-music-controls-plugin-v3"
export default class MediaSession {
initialize() {
CapacitorMusicControls.addListener("controlsNotification", (info) => {
console.log(info)
this.handleControlsEvent(info)
})
document.addEventListener("controlsNotification", (event) => {
console.log(event)
const info = { message: event.message, position: 0 }
this.handleControlsEvent(info)
})
}
update(manifest) {
try {
if ("mediaSession" in navigator) {
return navigator.mediaSession.metadata = new MediaMetadata({
title: manifest.title,
artist: manifest.artist,
album: manifest.album,
artwork: [
{
src: manifest.cover ?? manifest.thumbnail,
sizes: "512x512",
type: "image/jpeg",
}
],
})
}
return CapacitorMusicControls.create({
track: manifest.title,
artist: manifest.artist,
album: manifest.album,
cover: manifest.cover,
hasPrev: false,
hasNext: false,
hasClose: true,
isPlaying: true,
dismissable: false,
playIcon: "media_play",
pauseIcon: "media_pause",
prevIcon: "media_prev",
nextIcon: "media_next",
closeIcon: "media_close",
notificationIcon: "notification"
})
} catch (error) {
console.error(error)
}
}
updateIsPlaying(to, timeElapsed = 0) {
try {
if ("mediaSession" in navigator) {
return navigator.mediaSession.playbackState = to ? "playing" : "paused"
}
return CapacitorMusicControls.updateIsPlaying({
isPlaying: to,
elapsed: timeElapsed,
})
} catch {
console.error(error)
}
}
destroy() {
if ("mediaSession" in navigator) {
navigator.mediaSession.playbackState = "none"
}
this.active = false
return CapacitorMusicControls.destroy()
}
handleControlsEvent(action) {
try {
const message = action.message
switch (message) {
case "music-controls-next": {
return app.cores.player.playback.next()
}
case "music-controls-previous": {
return app.cores.player.playback.previous()
}
case "music-controls-pause": {
return app.cores.player.playback.pause()
}
case "music-controls-play": {
return app.cores.player.playback.play()
}
case "music-controls-destroy": {
return app.cores.player.playback.stop()
}
// External controls (iOS only)
case "music-controls-toggle-play-pause": {
return app.cores.player.playback.toggle()
}
// Headset events (Android only)
// All media button events are listed below
case "music-controls-media-button": {
return app.cores.player.playback.toggle()
}
case "music-controls-headset-unplugged": {
return app.cores.player.playback.pause()
}
case "music-controls-headset-plugged": {
return app.cores.player.playback.play()
}
default:
break;
}
} catch (error) {
console.error(error)
}
}
}

View File

@ -1,11 +1,8 @@
import React from "react" import React from "react"
import progressBar from "nprogress"
import Layouts from "@layouts" import Layouts from "@layouts"
export default class Layout extends React.PureComponent { export default class Layout extends React.PureComponent {
progressBar = progressBar.configure({ parent: "html", showSpinner: false })
state = { state = {
layoutType: "default", layoutType: "default",
renderError: null, renderError: null,
@ -17,14 +14,18 @@ export default class Layout extends React.PureComponent {
}, },
"layout.animations.fadeOut": () => { "layout.animations.fadeOut": () => {
if (app.cores.settings.get("reduceAnimations")) { if (app.cores.settings.get("reduceAnimations")) {
console.warn("Skipping fadeIn animation due to `reduceAnimations` setting") console.warn(
"Skipping fadeIn animation due to `reduceAnimations` setting",
)
return false return false
} }
const transitionLayer = document.getElementById("transitionLayer") const transitionLayer = document.getElementById("transitionLayer")
if (!transitionLayer) { if (!transitionLayer) {
console.warn("transitionLayer not found, no animation will be played") console.warn(
"transitionLayer not found, no animation will be played",
)
return false return false
} }
@ -32,19 +33,23 @@ export default class Layout extends React.PureComponent {
}, },
"layout.animations.fadeIn": () => { "layout.animations.fadeIn": () => {
if (app.cores.settings.get("reduceAnimations")) { if (app.cores.settings.get("reduceAnimations")) {
console.warn("Skipping fadeOut animation due to `reduceAnimations` setting") console.warn(
"Skipping fadeOut animation due to `reduceAnimations` setting",
)
return false return false
} }
const transitionLayer = document.getElementById("transitionLayer") const transitionLayer = document.getElementById("transitionLayer")
if (!transitionLayer) { if (!transitionLayer) {
console.warn("transitionLayer not found, no animation will be played") console.warn(
"transitionLayer not found, no animation will be played",
)
return false return false
} }
transitionLayer.classList.remove("fade-opacity-leave") transitionLayer.classList.remove("fade-opacity-leave")
} },
} }
componentDidMount() { componentDidMount() {
@ -58,7 +63,10 @@ export default class Layout extends React.PureComponent {
} }
if (app.cores.settings.get("reduceAnimations")) { if (app.cores.settings.get("reduceAnimations")) {
this.layoutInterface.toggleRootContainerClassname("reduce-animations", true) this.layoutInterface.toggleRootContainerClassname(
"reduce-animations",
true,
)
} }
this.layoutInterface.toggleCenteredContent(true) this.layoutInterface.toggleCenteredContent(true)
@ -75,7 +83,7 @@ export default class Layout extends React.PureComponent {
this.setState({ renderError: { info, stack } }) this.setState({ renderError: { info, stack } })
} }
layoutInterface = window.app.layout = { layoutInterface = (window.app.layout = {
set: (layout) => { set: (layout) => {
if (typeof Layouts[layout] !== "function") { if (typeof Layouts[layout] !== "function") {
return console.error("Layout not found") return console.error("Layout not found")
@ -88,28 +96,52 @@ export default class Layout extends React.PureComponent {
}) })
}, },
toggleCenteredContent: (to) => { toggleCenteredContent: (to) => {
return this.layoutInterface.toggleRootContainerClassname("centered-content", to) return this.layoutInterface.toggleRootContainerClassname(
"centered-content",
to,
)
}, },
toggleRootScaleEffect: (to) => { toggleRootScaleEffect: (to) => {
return this.layoutInterface.toggleRootContainerClassname("root-scale-effect", to) return this.layoutInterface.toggleRootContainerClassname(
"root-scale-effect",
to,
)
}, },
toggleMobileStyle: (to) => { toggleMobileStyle: (to) => {
return this.layoutInterface.toggleRootContainerClassname("mobile", to) return this.layoutInterface.toggleRootContainerClassname(
"mobile",
to,
)
}, },
toggleReducedAnimations: (to) => { toggleReducedAnimations: (to) => {
return this.layoutInterface.toggleRootContainerClassname("reduce-animations", to) return this.layoutInterface.toggleRootContainerClassname(
"reduce-animations",
to,
)
}, },
toggleTopBarSpacer: (to) => { toggleTopBarSpacer: (to) => {
return this.layoutInterface.toggleRootContainerClassname("top-bar-spacer", to) return this.layoutInterface.toggleRootContainerClassname(
"top-bar-spacer",
to,
)
}, },
toggleDisableTopLayoutPadding: (to) => { toggleDisableTopLayoutPadding: (to) => {
return this.layoutInterface.toggleRootContainerClassname("disable-top-layout-padding", to) return this.layoutInterface.toggleRootContainerClassname(
"disable-top-layout-padding",
to,
)
}, },
togglePagePanelSpacer: (to) => { togglePagePanelSpacer: (to) => {
return this.layoutInterface.toggleRootContainerClassname("page-panel-spacer", to) return this.layoutInterface.toggleRootContainerClassname(
"page-panel-spacer",
to,
)
}, },
toggleCompactMode: (to) => { toggleCompactMode: (to) => {
return this.layoutInterface.toggleRootContainerClassname("compact-mode", to) return this.layoutInterface.toggleRootContainerClassname(
"compact-mode",
to,
)
}, },
toggleRootContainerClassname: (classname, to) => { toggleRootContainerClassname: (classname, to) => {
const root = document.documentElement const root = document.documentElement
@ -119,7 +151,10 @@ export default class Layout extends React.PureComponent {
return false return false
} }
to = typeof to === "boolean" ? to : !root.classList.contains(classname) to =
typeof to === "boolean"
? to
: !root.classList.contains(classname)
if (root.classList.contains(classname) === to) { if (root.classList.contains(classname) === to) {
// ignore // ignore
@ -154,8 +189,8 @@ export default class Layout extends React.PureComponent {
...to, ...to,
behavior: "smooth", behavior: "smooth",
}) })
} },
} })
render() { render() {
let layoutType = this.state.layoutType let layoutType = this.state.layoutType
@ -167,7 +202,10 @@ export default class Layout extends React.PureComponent {
if (this.state.renderError) { if (this.state.renderError) {
if (this.props.staticRenders?.RenderError) { if (this.props.staticRenders?.RenderError) {
return React.createElement(this.props.staticRenders?.RenderError, { error: this.state.renderError }) return React.createElement(
this.props.staticRenders?.RenderError,
{ error: this.state.renderError },
)
} }
return JSON.stringify(this.state.renderError) return JSON.stringify(this.state.renderError)
@ -176,11 +214,12 @@ export default class Layout extends React.PureComponent {
const Layout = Layouts[layoutType] const Layout = Layouts[layoutType]
if (!Layout) { if (!Layout) {
return app.eventBus.emit("runtime.crash", new Error(`Layout type [${layoutType}] not found`)) return app.eventBus.emit(
"runtime.crash",
new Error(`Layout type [${layoutType}] not found`),
)
} }
return <Layout {...layoutComponentProps}> return <Layout {...layoutComponentProps}>{this.props.children}</Layout>
{this.props.children}
</Layout>
} }
} }

View File

@ -1,13 +1,16 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import classnames from "classnames" import classnames from "classnames"
import { Motion, spring } from "react-motion" import { motion, AnimatePresence } from "motion/react"
import { Icons, createIconRender } from "@components/Icons" import { Icons, createIconRender } from "@components/Icons"
import { WithPlayerContext, Context } from "@contexts/WithPlayerContext" import { WithPlayerContext, Context } from "@contexts/WithPlayerContext"
import { QuickNavMenuItems, QuickNavMenu } from "@layouts/components/@mobile/quickNav" import {
QuickNavMenuItems,
QuickNavMenu,
} from "@layouts/components/@mobile/quickNav"
import PlayerView from "@pages/@mobile-views/player" import PlayerView from "@pages/@mobile-views/player"
import CreatorView from "@pages/@mobile-views/creator" import CreatorView from "@pages/@mobile-views/creator"
@ -23,10 +26,11 @@ const tourSteps = [
}, },
{ {
title: "Account button", title: "Account button",
description: "Tap & hold on the account icon to open miscellaneous options.", description:
"Tap & hold on the account icon to open miscellaneous options.",
placement: "top", placement: "top",
refName: "accountBtnRef", refName: "accountBtnRef",
} },
] ]
const openPlayerView = () => { const openPlayerView = () => {
@ -41,23 +45,26 @@ const PlayerButton = (props) => {
openPlayerView() openPlayerView()
}, []) }, [])
return <div return (
className={classnames( <div
"player_btn", className={classnames("player_btn", {
{ bounce: props.playback === "playing",
"bounce": props.playback === "playing" })}
}
)}
style={{ style={{
"--average-color": props.colorAnalysis?.rgba, "--average-color": props.colorAnalysis?.rgba,
"--color": props.colorAnalysis?.isDark ? "var(--text-color-white)" : "var(--text-color-black)", "--color": props.colorAnalysis?.isDark
? "var(--text-color-white)"
: "var(--text-color-black)",
}} }}
onClick={openPlayerView} onClick={openPlayerView}
> >
{ {props.playback === "playing" ? (
props.playback === "playing" ? <Icons.MdMusicNote /> : <Icons.MdPause /> <Icons.MdMusicNote />
} ) : (
<Icons.MdPause />
)}
</div> </div>
)
} }
const AccountButton = React.forwardRef((props, ref) => { const AccountButton = React.forwardRef((props, ref) => {
@ -98,12 +105,13 @@ const AccountButton = React.forwardRef((props, ref) => {
onClick: () => { onClick: () => {
app.eventBus.emit("app.logout_request") app.eventBus.emit("app.logout_request")
}, },
} },
] ],
}) })
} }
return <div return (
<div
key="account" key="account"
id="account" id="account"
className="item" className="item"
@ -113,11 +121,14 @@ const AccountButton = React.forwardRef((props, ref) => {
context-menu="ignore" context-menu="ignore"
> >
<div className="icon"> <div className="icon">
{ {user ? (
user ? <antd.Avatar shape="square" src={app.userData.avatar} /> : createIconRender("FiLogin") <antd.Avatar shape="square" src={app.userData.avatar} />
} ) : (
createIconRender("FiLogin")
)}
</div> </div>
</div> </div>
)
}) })
export class BottomBar extends React.Component { export class BottomBar extends React.Component {
@ -133,7 +144,7 @@ export class BottomBar extends React.Component {
busEvents = { busEvents = {
"runtime.crash": () => { "runtime.crash": () => {
this.toggleVisibility(false) this.toggleVisibility(false)
} },
} }
navBtnRef = React.createRef() navBtnRef = React.createRef()
@ -308,50 +319,63 @@ export class BottomBar extends React.Component {
} }
this.setState({ this.setState({
tourOpen: to tourOpen: to,
}) })
} }
render() { render() {
if (this.state.render) { if (this.state.render) {
return <div className="bottomBar"> return <div className="bottomBar">{this.state.render}</div>
{this.state.render}
</div>
} }
const heightValue = this.state.visible ? Number(app.cores.style.getDefaultVar("bottom-bar-height").replace("px", "")) : 0 const heightValue = Number(
app.cores.style
.getDefaultVar("bottom-bar-height")
.replace("px", ""),
)
return <> return (
{ <>
this.state.tourOpen && <antd.Tour {this.state.tourOpen && (
<antd.Tour
open open
steps={this.getTourSteps()} steps={this.getTourSteps()}
onClose={() => this.setState({ tourOpen: false })} onClose={() => this.setState({ tourOpen: false })}
/> />
} )}
<QuickNavMenu <QuickNavMenu visible={this.state.quickNavVisible} />
visible={this.state.quickNavVisible}
/>
<Motion style={{ <AnimatePresence>
y: spring(this.state.visible ? 0 : 300), {this.state.visible && (
height: spring(heightValue) <motion.div
}}> className="bottomBar_wrapper"
{({ y, height }) => initial={{
<div className="bottomBar_wrapper"> height: 0,
<div y: 300,
className="bottomBar" }}
style={{ animate={{
WebkitTransform: `translateY(${y}px)`, height: heightValue,
transform: `translateY(${y}px)`, y: 0,
height: `${height}px`, }}
exit={{
height: 0,
y: 300,
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}} }}
> >
<div className="bottomBar">
<div className="items"> <div className="items">
<div <div
key="creator" key="creator"
id="creator" id="creator"
className={classnames("item", "primary")} className={classnames(
"item",
"primary",
)}
onClick={openCreator} onClick={openCreator}
> >
<div className="icon"> <div className="icon">
@ -359,17 +383,22 @@ export class BottomBar extends React.Component {
</div> </div>
</div> </div>
{ {this.context.track_manifest && (
this.context.track_manifest && <div <div className="item">
className="item"
>
<PlayerButton <PlayerButton
manifest={this.context.track_manifest} manifest={
playback={this.context.playback_status} this.context.track_manifest
colorAnalysis={this.context.track_manifest?.cover_analysis} }
playback={
this.context.playback_status
}
colorAnalysis={
this.context.track_manifest
?.cover_analysis
}
/> />
</div> </div>
} )}
<div <div
key="navigator" key="navigator"
@ -381,7 +410,9 @@ export class BottomBar extends React.Component {
onTouchStart={this.handleNavTouchStart} onTouchStart={this.handleNavTouchStart}
onTouchEnd={this.handleNavTouchEnd} onTouchEnd={this.handleNavTouchEnd}
onTouchCancel={() => { onTouchCancel={() => {
this.setState({ quickNavVisible: false }) this.setState({
quickNavVisible: false,
})
}} }}
> >
<div className="icon"> <div className="icon">
@ -400,22 +431,21 @@ export class BottomBar extends React.Component {
</div> </div>
</div> </div>
<AccountButton <AccountButton ref={this.accountBtnRef} />
ref={this.accountBtnRef}
/>
</div> </div>
</div> </div>
</div> </motion.div>
} )}
</Motion> </AnimatePresence>
</> </>
)
} }
} }
export default (props) => { export default (props) => {
return <WithPlayerContext> return (
<BottomBar <WithPlayerContext>
{...props} <BottomBar {...props} />
/>
</WithPlayerContext> </WithPlayerContext>
)
} }

View File

@ -1,6 +1,6 @@
import React from "react" import React from "react"
import classnames from "classnames" import classnames from "classnames"
import { Motion, spring } from "react-motion" import { motion, AnimatePresence } from "motion/react"
import useLayoutInterface from "@hooks/useLayoutInterface" import useLayoutInterface from "@hooks/useLayoutInterface"
import useDefaultVisibility from "@hooks/useDefaultVisibility" import useDefaultVisibility from "@hooks/useDefaultVisibility"
@ -9,7 +9,8 @@ import "./index.less"
export default (props) => { export default (props) => {
const [visible, setVisible] = useDefaultVisibility() const [visible, setVisible] = useDefaultVisibility()
const [shouldUseTopBarSpacer, setShouldUseTopBarSpacer] = React.useState(true) const [shouldUseTopBarSpacer, setShouldUseTopBarSpacer] =
React.useState(true)
const [render, setRender] = React.useState(null) const [render, setRender] = React.useState(null)
useLayoutInterface("top_bar", { useLayoutInterface("top_bar", {
@ -31,7 +32,7 @@ export default (props) => {
shouldUseTopBarSpacer: (to) => { shouldUseTopBarSpacer: (to) => {
app.layout.toggleTopBarSpacer(to) app.layout.toggleTopBarSpacer(to)
setShouldUseTopBarSpacer(to) setShouldUseTopBarSpacer(to)
} },
}) })
const handleUpdateRender = (...args) => { const handleUpdateRender = (...args) => {
@ -47,7 +48,7 @@ export default (props) => {
const updateRender = (component, options = {}) => { const updateRender = (component, options = {}) => {
setRender({ setRender({
component, component,
options options,
}) })
} }
@ -85,20 +86,31 @@ export default (props) => {
} }
}, [render]) }, [render])
const heightValue = visible ? Number(app.cores.style.getDefaultVar("top-bar-height").replace("px", "")) : 0 const heightValue = Number(
app.cores.style.getDefaultVar("top-bar-height").replace("px", ""),
)
return <Motion style={{ return (
y: spring(visible ? 0 : 300,), <AnimatePresence>
height: spring(heightValue,), {visible && (
}}> <motion.div
{({ y, height }) => {
return <>
<div
className="top-bar_wrapper" className="top-bar_wrapper"
style={{ animate={{
WebkitTransform: `translateY(-${y}px)`, y: 0,
transform: `translateY(-${y}px)`, height: heightValue,
height: `${height}px`, }}
initial={{
y: -300,
height: 0,
}}
exit={{
y: -300,
height: 0,
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}} }}
> >
<div <div
@ -107,12 +119,14 @@ export default (props) => {
render?.options?.className, render?.options?.className,
)} )}
> >
{ {render?.component &&
render?.component && React.cloneElement(render?.component, render?.options?.props ?? {}) React.cloneElement(
} render?.component,
render?.options?.props ?? {},
)}
</div> </div>
</div> </motion.div>
</> )}
}} </AnimatePresence>
</Motion> )
} }

View File

@ -1,7 +1,6 @@
import React from "react" import React from "react"
import classnames from "classnames" import classnames from "classnames"
import * as antd from "antd" import { AnimatePresence, motion } from "motion/react"
import { AnimatePresence, motion } from "framer-motion"
import "./index.less" import "./index.less"
@ -30,7 +29,7 @@ export class Drawer extends React.Component {
this.toggleVisibility(false) this.toggleVisibility(false)
this.props.controller.close(this.props.id, { this.props.controller.close(this.props.id, {
transition: 150 transition: 150,
}) })
} }
@ -65,9 +64,10 @@ export class Drawer extends React.Component {
handleDone: this.handleDone, handleDone: this.handleDone,
handleFail: this.handleFail, handleFail: this.handleFail,
} }
return <AnimatePresence> return (
{ <AnimatePresence>
this.state.visible && <motion.div {this.state.visible && (
<motion.div
key={this.props.id} key={this.props.id}
id={this.props.id} id={this.props.id}
className="drawer" className="drawer"
@ -84,12 +84,14 @@ export class Drawer extends React.Component {
opacity: [1, 0], opacity: [1, 0],
}} }}
> >
{ {React.createElement(
React.createElement(this.props.children, componentProps) this.props.children,
} componentProps,
)}
</motion.div> </motion.div>
} )}
</AnimatePresence> </AnimatePresence>
)
} }
} }
@ -171,10 +173,12 @@ export default class DrawerController extends React.Component {
if (lastDrawer) { if (lastDrawer) {
if (app.layout.modal && lastDrawer.options.confirmOnOutsideClick) { if (app.layout.modal && lastDrawer.options.confirmOnOutsideClick) {
return app.layout.modal.confirm({ return app.layout.modal.confirm({
descriptionText: lastDrawer.options.confirmOnOutsideClickText ?? "Are you sure you want to close this drawer?", descriptionText:
lastDrawer.options.confirmOnOutsideClickText ??
"Are you sure you want to close this drawer?",
onConfirm: () => { onConfirm: () => {
lastDrawer.close() lastDrawer.close()
} },
}) })
} }
@ -202,18 +206,12 @@ export default class DrawerController extends React.Component {
} }
if (typeof addresses[id] === "undefined") { if (typeof addresses[id] === "undefined") {
drawers.push(<Drawer drawers.push(<Drawer key={id} {...instance} />)
key={id}
{...instance}
/>)
addresses[id] = drawers.length - 1 addresses[id] = drawers.length - 1
refs[id] = instance.ref refs[id] = instance.ref
} else { } else {
drawers[addresses[id]] = <Drawer drawers[addresses[id]] = <Drawer key={id} {...instance} />
key={id}
{...instance}
/>
refs[id] = instance.ref refs[id] = instance.ref
} }
@ -267,10 +265,11 @@ export default class DrawerController extends React.Component {
} }
render() { render() {
return <> return (
<>
<AnimatePresence> <AnimatePresence>
{ {this.state.maskVisible && (
this.state.maskVisible && <motion.div <motion.div
className="drawers-mask" className="drawers-mask"
onClick={() => this.closeLastDrawer()} onClick={() => this.closeLastDrawer()}
initial={{ initial={{
@ -282,22 +281,23 @@ export default class DrawerController extends React.Component {
exit={{ exit={{
opacity: 0, opacity: 0,
}} }}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}}
/> />
} )}
</AnimatePresence> </AnimatePresence>
<div <div
className={classnames( className={classnames("drawers-wrapper", {
"drawers-wrapper",
{
["hidden"]: !this.state.drawers.length, ["hidden"]: !this.state.drawers.length,
} })}
)}
> >
<AnimatePresence> <AnimatePresence>{this.state.drawers}</AnimatePresence>
{this.state.drawers}
</AnimatePresence>
</div> </div>
</> </>
)
} }
} }

View File

@ -1,6 +1,6 @@
import React from "react" import React from "react"
import classnames from "classnames" import classnames from "classnames"
import { Motion, spring } from "react-motion" import { motion, AnimatePresence } from "motion/react"
import useLayoutInterface from "@hooks/useLayoutInterface" import useLayoutInterface from "@hooks/useLayoutInterface"
@ -17,7 +17,7 @@ export default (props) => {
return setRender({ return setRender({
component, component,
options options,
}) })
}, },
}) })
@ -30,35 +30,35 @@ export default (props) => {
} }
}, [render]) }, [render])
return <Motion return (
style={{ <AnimatePresence>
y: spring(render ? 0 : 100,), {render && (
<motion.div
className="page_header_wrapper"
animate={{
y: 0,
}}
initial={{
y: -100,
}}
exit={{
y: -100,
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}} }}
> >
{({ y, height }) => { <div className="page_header">
return <div {render?.component &&
className={classnames( React.cloneElement(
"page_header_wrapper",
{
["hidden"]: !render,
}
)}
style={{
WebkitTransform: `translateY(-${y}px)`,
transform: `translateY(-${y}px)`,
}}
>
<div
className="page_header"
>
{
render?.component && React.cloneElement(
render?.component, render?.component,
render?.options?.props ?? {} render?.options?.props ?? {},
)}
</div>
</motion.div>
)}
</AnimatePresence>
) )
} }
</div>
</div>
}}
</Motion>
}

View File

@ -2,7 +2,7 @@ import React from "react"
import config from "@config" import config from "@config"
import classnames from "classnames" import classnames from "classnames"
import { Translation } from "react-i18next" import { Translation } from "react-i18next"
import { motion, AnimatePresence } from "framer-motion" import { motion, AnimatePresence } from "motion/react"
import { Menu, Avatar, Dropdown, Tag } from "antd" import { Menu, Avatar, Dropdown, Tag } from "antd"
import Drawer from "@layouts/components/drawer" import Drawer from "@layouts/components/drawer"

View File

@ -1,6 +1,6 @@
import React from "react" import React from "react"
import classnames from "classnames" import classnames from "classnames"
import { Motion, spring } from "react-motion" import { motion, AnimatePresence } from "motion/react"
import WidgetsWrapper from "@components/WidgetsWrapper" import WidgetsWrapper from "@components/WidgetsWrapper"
@ -51,69 +51,83 @@ export default class ToolsBar extends React.Component {
detachRender: (id) => { detachRender: (id) => {
this.setState({ this.setState({
renders: { renders: {
top: this.state.renders.top.filter((render) => render.id !== id), top: this.state.renders.top.filter(
bottom: this.state.renders.bottom.filter((render) => render.id !== id), (render) => render.id !== id,
),
bottom: this.state.renders.bottom.filter(
(render) => render.id !== id,
),
}, },
}) })
return true return true
} },
} }
render() { render() {
const hasAnyRenders = this.state.renders.top.length > 0 || this.state.renders.bottom.length > 0 const hasAnyRenders =
this.state.renders.top.length > 0 ||
this.state.renders.bottom.length > 0
const isVisible = hasAnyRenders && this.state.visible const isVisible = hasAnyRenders && this.state.visible
return <Motion return (
style={{ <AnimatePresence>
x: spring(isVisible ? 0 : 100), {isVisible && (
width: spring(isVisible ? 100 : 0), <motion.div
className={classnames("tools-bar-wrapper", {
["visible"]: isVisible,
})}
animate={{
x: 0,
width: "100%",
}}
initial={{
x: 100,
width: "0%",
}}
exit={{
x: 100,
width: "0%",
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}} }}
> >
{({ x, width }) => { <div id="tools_bar" className="tools-bar">
return <div
style={{
width: `${width}%`,
transform: `translateX(${x}%)`,
}}
className={classnames(
"tools-bar-wrapper",
{
visible: isVisible,
}
)}
>
<div
id="tools_bar"
className="tools-bar"
>
<div className="attached_renders top"> <div className="attached_renders top">
{this.state.renders.top.map((render, index) => {
return React.createElement(
render.component,
{ {
this.state.renders.top.map((render, index) => {
return React.createElement(render.component, {
...render.props, ...render.props,
key: index, key: index,
}) },
}) )
} })}
</div> </div>
<WidgetsWrapper /> <WidgetsWrapper />
<div className="attached_renders bottom"> <div className="attached_renders bottom">
{this.state.renders.bottom.map(
(render, index) => {
return React.createElement(
render.component,
{ {
this.state.renders.bottom.map((render, index) => {
return React.createElement(render.component, {
...render.props, ...render.props,
key: index, key: index,
}) },
}) )
} },
)}
</div> </div>
</div> </div>
</div> </motion.div>
}} )}
</Motion> </AnimatePresence>
)
} }
} }

View File

@ -21,7 +21,6 @@
&:not(.visible) { &:not(.visible) {
min-width: 0; min-width: 0;
padding: 0; padding: 0;
} }
} }

View File

@ -1,7 +1,7 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import classnames from "classnames" import classnames from "classnames"
import { motion, AnimatePresence } from "framer-motion" import { motion, AnimatePresence } from "motion/react"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import FollowButton from "@components/FollowButton" import FollowButton from "@components/FollowButton"
@ -21,10 +21,10 @@ import FollowersTab from "./tabs/followers"
import "./index.less" import "./index.less"
const TabsComponent = { const TabsComponent = {
"posts": PostsTab, posts: PostsTab,
"followers": FollowersTab, followers: FollowersTab,
"details": DetailsTab, details: DetailsTab,
"music": MusicTab, music: MusicTab,
} }
export default class Account extends React.Component { export default class Account extends React.Component {
@ -60,7 +60,7 @@ export default class Account extends React.Component {
} }
user = await UserModel.data({ user = await UserModel.data({
username: requestedUser username: requestedUser,
}).catch((error) => { }).catch((error) => {
console.error(error) console.error(error)
@ -77,7 +77,9 @@ export default class Account extends React.Component {
console.log(`Loaded User [${user.username}] >`, user) console.log(`Loaded User [${user.username}] >`, user)
const followersResult = await FollowsModel.getFollowers(user._id).catch(() => false) const followersResult = await FollowsModel.getFollowers(
user._id,
).catch(() => false)
if (followersResult) { if (followersResult) {
followersCount = followersResult.count followersCount = followersResult.count
@ -118,7 +120,10 @@ export default class Account extends React.Component {
handlePageTransition = (key) => { handlePageTransition = (key) => {
if (typeof key !== "string") { if (typeof key !== "string") {
console.error("Cannot handle page transition. Invalid key, only valid passing string", key) console.error(
"Cannot handle page transition. Invalid key, only valid passing string",
key,
)
return return
} }
@ -129,7 +134,7 @@ export default class Account extends React.Component {
} }
this.setState({ this.setState({
tabActiveKey: key tabActiveKey: key,
}) })
} }
@ -137,43 +142,39 @@ export default class Account extends React.Component {
const user = this.state.user const user = this.state.user
if (this.state.isNotExistent) { if (this.state.isNotExistent) {
return <antd.Result return (
<antd.Result
status="404" status="404"
title="This user does not exist, yet..." title="This user does not exist, yet..."
> ></antd.Result>
)
</antd.Result>
} }
if (!user) { if (!user) {
return <antd.Skeleton active /> return <antd.Skeleton active />
} }
return <div return (
<div
id="profile" id="profile"
className={classnames( className={classnames("account-profile", {
"account-profile",
{
["withCover"]: user.cover, ["withCover"]: user.cover,
} })}
)}
> >
{ {user.cover && (
user.cover && <div <div
className={classnames("cover", { className={classnames("cover", {
["expanded"]: this.state.coverExpanded ["expanded"]: this.state.coverExpanded,
})} })}
style={{ backgroundImage: `url("${user.cover}")` }} style={{ backgroundImage: `url("${user.cover}")` }}
onClick={() => this.toggleCoverExpanded()} onClick={() => this.toggleCoverExpanded()}
id="profile-cover" id="profile-cover"
/> />
} )}
<div className="panels"> <div className="panels">
<div className="left-panel"> <div className="left-panel">
<UserCard <UserCard user={user} />
user={user}
/>
<div className="actions"> <div className="actions">
<FollowButton <FollowButton
@ -183,19 +184,20 @@ export default class Account extends React.Component {
self={this.state.isSelf} self={this.state.isSelf}
/> />
{ {!this.state.isSelf && (
!this.state.isSelf && <antd.Button <antd.Button
icon={<Icons.MdMessage />} icon={<Icons.MdMessage />}
onClick={() => app.location.push(`/messages/${user._id}`)} onClick={() =>
/> app.location.push(
`/messages/${user._id}`,
)
} }
/>
)}
</div> </div>
</div> </div>
<div <div className="center-panel" ref={this.contentRef}>
className="center-panel"
ref={this.contentRef}
>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, scale: 0.95 }}
@ -209,12 +211,14 @@ export default class Account extends React.Component {
width: "100%", width: "100%",
}} }}
> >
{React.createElement(
TabsComponent[this.state.tabActiveKey],
{ {
React.createElement(TabsComponent[this.state.tabActiveKey], { onTopVisibility:
onTopVisibility: this.onPostListTopVisibility, this.onPostListTopVisibility,
state: this.state state: this.state,
}) },
} )}
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
</div> </div>
@ -245,11 +249,12 @@ export default class Account extends React.Component {
id: "details", id: "details",
label: "Details", label: "Details",
icon: "FiInfo", icon: "FiInfo",
} },
])} ])}
/> />
</div> </div>
</div> </div>
</div> </div>
)
} }
} }

View File

@ -1,6 +1,6 @@
import React from "react" import React from "react"
import classnames from "classnames" import classnames from "classnames"
import { Motion, spring } from "react-motion" import { motion, AnimatePresence } from "motion/react"
import { usePlayerStateContext } from "@contexts/WithPlayerContext" import { usePlayerStateContext } from "@contexts/WithPlayerContext"
@ -17,7 +17,10 @@ const LyricsText = React.forwardRef((props, textRef) => {
const currentTrackTime = app.cores.player.controls.seek() * 1000 const currentTrackTime = app.cores.player.controls.seek() * 1000
const lineIndex = lyrics.synced_lyrics.findIndex((line) => { const lineIndex = lyrics.synced_lyrics.findIndex((line) => {
return currentTrackTime >= line.startTimeMs && currentTrackTime <= line.endTimeMs return (
currentTrackTime >= line.startTimeMs &&
currentTrackTime <= line.endTimeMs
)
}) })
if (lineIndex === -1) { if (lineIndex === -1) {
@ -72,7 +75,9 @@ const LyricsText = React.forwardRef((props, textRef) => {
setVisible(true) setVisible(true)
// find line element by id // find line element by id
const lineElement = textRef.current.querySelector(`#lyrics-line-${currentLineIndex}`) const lineElement = textRef.current.querySelector(
`#lyrics-line-${currentLineIndex}`,
)
// center scroll to current line // center scroll to current line
if (lineElement) { if (lineElement) {
@ -112,42 +117,46 @@ const LyricsText = React.forwardRef((props, textRef) => {
return null return null
} }
return <div return (
className="lyrics-text-wrapper" <div className="lyrics-text-wrapper">
> <AnimatePresence>
<Motion {visible && (
style={{ <motion.div
opacity: spring(visible ? 1 : 0),
}}
>
{({ opacity }) => {
return <div
ref={textRef} ref={textRef}
className="lyrics-text" className="lyrics-text"
style={{ animate={{
opacity opacity: 1,
}}
initial={{
opacity: 0,
}}
exit={{
opacity: 0,
}}
transition={{
type: "spring",
stiffness: 100,
damping: 20,
}} }}
> >
{ {lyrics.synced_lyrics.map((line, index) => {
lyrics.synced_lyrics.map((line, index) => { return (
return <p <p
key={index} key={index}
id={`lyrics-line-${index}`} id={`lyrics-line-${index}`}
className={classnames( className={classnames("line", {
"line", ["current"]: currentLineIndex === index,
{ })}
["current"]: currentLineIndex === index
}
)}
> >
{line.text} {line.text}
</p> </p>
}) )
} })}
</div> </motion.div>
}} )}
</Motion> </AnimatePresence>
</div> </div>
)
}) })
export default LyricsText export default LyricsText

View File

@ -11,8 +11,10 @@ import "./index.less"
const connectionsTooltipStrings = { const connectionsTooltipStrings = {
secure: "This connection is secure", secure: "This connection is secure",
insecure: "This connection is insecure, cause it's not using HTTPS protocol and the server cannot be verified on the trusted certificate authority.", insecure:
warning: "This connection is secure but the server cannot be verified on the trusted certificate authority.", "This connection is insecure, cause it's not using HTTPS protocol and the server cannot be verified on the trusted certificate authority.",
warning:
"This connection is secure but the server cannot be verified on the trusted certificate authority.",
} }
export default { export default {
@ -29,12 +31,14 @@ export default {
const [capInfo, setCapInfo] = React.useState(null) const [capInfo, setCapInfo] = React.useState(null)
const setCapacitorInfo = async () => { const setCapacitorInfo = async () => {
if (Capacitor) {
if (Capacitor.Plugins.App) { if (Capacitor.Plugins.App) {
const info = await Capacitor.Plugins.App.getInfo() const info = await Capacitor.Plugins.App.getInfo()
setCapInfo(info) setCapInfo(info)
} }
} }
}
const checkServerVersion = async () => { const checkServerVersion = async () => {
const serverManifest = await app.cores.api.customRequest() const serverManifest = await app.cores.api.customRequest()
@ -61,14 +65,12 @@ export default {
setCapacitorInfo() setCapacitorInfo()
}, []) }, [])
return <div className="about_app"> return (
<div className="about_app">
<div className="header"> <div className="header">
<div className="branding"> <div className="branding">
<div className="logo"> <div className="logo">
<img <img src={config.logo.alt} alt="Logo" />
src={config.logo.alt}
alt="Logo"
/>
</div> </div>
<div className="texts"> <div className="texts">
<div className="sitename-text"> <div className="sitename-text">
@ -76,13 +78,23 @@ export default {
<antd.Tag>Beta</antd.Tag> <antd.Tag>Beta</antd.Tag>
</div> </div>
<span>{config.author}</span> <span>{config.author}</span>
<span>Licensed with {config.package?.license ?? "unlicensed"} </span> <span>
Licensed with{" "}
{config.package?.license ?? "unlicensed"}{" "}
</span>
</div> </div>
</div> </div>
<div className="versions"> <div className="versions">
<antd.Tag><Icons.FiTag />v{window.app.version ?? "experimental"}</antd.Tag> <antd.Tag>
<Icons.FiTag />v
{window.app.version ?? "experimental"}
</antd.Tag>
<antd.Tag color={isProduction ? "green" : "magenta"}> <antd.Tag color={isProduction ? "green" : "magenta"}>
{isProduction ? <Icons.FiCheckCircle /> : <Icons.FiTriangle />} {isProduction ? (
<Icons.FiCheckCircle />
) : (
<Icons.FiTriangle />
)}
{String(import.meta.env.MODE)} {String(import.meta.env.MODE)}
</antd.Tag> </antd.Tag>
</div> </div>
@ -90,23 +102,38 @@ export default {
<div className="group"> <div className="group">
<div className="group_header"> <div className="group_header">
<h3><Icons.FiInfo />Server information</h3> <h3>
<Icons.FiInfo />
Server information
</h3>
</div> </div>
<div className="field"> <div className="field">
<div className="field_header"> <div className="field_header">
<h3><Icons.MdOutlineStream /> Origin</h3> <h3>
<Icons.MdOutlineStream /> Origin
</h3>
<antd.Tooltip <antd.Tooltip
title={secureConnection ? connectionsTooltipStrings.secure : connectionsTooltipStrings.insecure} title={
secureConnection
? connectionsTooltipStrings.secure
: connectionsTooltipStrings.insecure
}
> >
<antd.Tag <antd.Tag
color={secureConnection ? "green" : "red"} color={secureConnection ? "green" : "red"}
icon={secureConnection ? <Icons.MdHttps /> : <Icons.MdWarning />} icon={
> secureConnection ? (
{ <Icons.MdHttps />
secureConnection ? " Secure connection" : "Insecure connection" ) : (
<Icons.MdWarning />
)
} }
>
{secureConnection
? " Secure connection"
: "Insecure connection"}
</antd.Tag> </antd.Tag>
</antd.Tooltip> </antd.Tooltip>
</div> </div>
@ -118,7 +145,9 @@ export default {
<div className="field"> <div className="field">
<div className="field_header"> <div className="field_header">
<h3><Icons.MdOutlineMemory /> Instance Performance</h3> <h3>
<Icons.MdOutlineMemory /> Instance Performance
</h3>
</div> </div>
<div className="field_value"> <div className="field_value">
@ -132,13 +161,9 @@ export default {
width: "100%", width: "100%",
}} }}
> >
<LatencyIndicator <LatencyIndicator type="http" />
type="http"
/>
<LatencyIndicator <LatencyIndicator type="ws" />
type="ws"
/>
</div> </div>
</div> </div>
</div> </div>
@ -173,9 +198,7 @@ export default {
<p>Platform</p> <p>Platform</p>
</div> </div>
<div className="field_value"> <div className="field_value">Web App</div>
{Capacitor.platform}
</div>
</div> </div>
<div className="inline_field"> <div className="inline_field">
@ -220,8 +243,8 @@ export default {
</div> </div>
</div> </div>
{ {capInfo && (
capInfo && <div className="inline_field"> <div className="inline_field">
<div className="field_header"> <div className="field_header">
<div className="field_icon"> <div className="field_icon">
<Icons.MdInfo /> <Icons.MdInfo />
@ -230,14 +253,12 @@ export default {
<p>App ID</p> <p>App ID</p>
</div> </div>
<div className="field_value"> <div className="field_value">{capInfo.id}</div>
{capInfo.id}
</div> </div>
</div> )}
}
{ {capInfo && (
capInfo && <div className="inline_field"> <div className="inline_field">
<div className="field_header"> <div className="field_header">
<div className="field_icon"> <div className="field_icon">
<Icons.MdInfo /> <Icons.MdInfo />
@ -246,14 +267,12 @@ export default {
<p>App Build</p> <p>App Build</p>
</div> </div>
<div className="field_value"> <div className="field_value">{capInfo.build}</div>
{capInfo.build}
</div> </div>
</div> )}
}
{ {capInfo && (
capInfo && <div className="inline_field"> <div className="inline_field">
<div className="field_header"> <div className="field_header">
<div className="field_icon"> <div className="field_icon">
<Icons.MdInfo /> <Icons.MdInfo />
@ -262,11 +281,9 @@ export default {
<p>App Version</p> <p>App Version</p>
</div> </div>
<div className="field_value"> <div className="field_value">{capInfo.version}</div>
{capInfo.version}
</div> </div>
</div> )}
}
<div className="inline_field"> <div className="inline_field">
<div className="field_header"> <div className="field_header">
@ -286,5 +303,6 @@ export default {
</div> </div>
</div> </div>
</div> </div>
} )
},
} }

View File

@ -11,9 +11,7 @@ const oneYearInSeconds = 60 * 60 * 24 * 365
const sslDirPath = path.join(__dirname, ".ssl") const sslDirPath = path.join(__dirname, ".ssl")
const config = { const config = {
plugins: [ plugins: [react()],
react(),
],
resolve: { resolve: {
alias: aliases, alias: aliases,
}, },
@ -24,7 +22,7 @@ const config = {
allow: ["..", "../../"], allow: ["..", "../../"],
}, },
headers: { headers: {
"Strict-Transport-Security": `max-age=${oneYearInSeconds}` "Strict-Transport-Security": `max-age=${oneYearInSeconds}`,
}, },
proxy: { proxy: {
"/api": { "/api": {
@ -36,38 +34,16 @@ const config = {
ws: true, ws: true,
toProxy: true, toProxy: true,
secure: false, secure: false,
} },
} },
}, },
css: { css: {
preprocessorOptions: { preprocessorOptions: {
less: { less: {
javascriptEnabled: true, javascriptEnabled: true,
}
}
}, },
optimizeDeps: {
esbuildOptions: {
target: "esnext"
}
}, },
build: {
target: "esnext",
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("node_modules")) {
return id.toString().split("node_modules/")[1].split("/")[0].toString()
}
}
}
}
}, },
esbuild: {
supported: {
"top-level-await": true
},
}
} }
if (fs.existsSync(sslDirPath)) { if (fs.existsSync(sslDirPath)) {