*update: contextual menu

*upadate: theme handlers & darkmodes

*added: window controllers api
This commit is contained in:
srgooglo 2020-09-19 01:32:54 +02:00
parent a863864644
commit c75f0744c2
17 changed files with 285 additions and 154 deletions

View File

@ -1,5 +1,6 @@
module.exports = {
app_config: {
id: "comty",
siteName: 'Comty',
copyright: 'RageStudio©',
MainPath: '/',
@ -12,6 +13,7 @@ module.exports = {
api_prefix: 'ycorejs_apiv3',
app_settings_storage: 'app_settings',
endpoint_global: 'https://comty.pw',
proxy_local: 'http://localhost:8000',
session_token_storage: 'cid',
session_data_storage: 'data',

14
globals/contextMenu.js Normal file
View File

@ -0,0 +1,14 @@
import * as Icons from 'components/Icons'
export default [
{
key: "inspect_element",
title: "Inspect",
icon: <Icons.Command />,
params: {
onClick: (e) => {
window.inspectElement(e)
}
}
}
]

View File

@ -20,8 +20,9 @@ const packagejson = require('../package.json')
const is = require('electron-is')
const waitOn = require('wait-on');
const { getDoNotDisturb } = require('electron-notification-state');
const { app_config } = require("../config");
let app_path = is.dev()? 'http://127.0.0.1:8000/' : `file://${path.join(__dirname, '..', 'renderer')}/index.html`;
let app_path = is.dev()? app_config.proxy_local : `file://${path.join(__dirname, '..', 'renderer')}/index.html`;
let mainWindow;
let tray;
let watcher;
@ -272,6 +273,5 @@ ipcMain.handle('contextualMenu', (event, payload) => {
})
ipcMain.handle('inspectElement', (event, payload) => {
log.log(payload)
mainWindow.inspectElement(payload.x, payload.y)
})

View File

@ -51,6 +51,7 @@
"electron-updater": "^4.3.4",
"enquire-js": "^0.2.1",
"feather-reactjs": "^2.0.13",
"html2canvas": "^1.0.0-rc.7",
"jquery": "^3.5.1",
"jsonwebtoken": "^8.5.1",
"less-vars-to-js": "^1.3.0",

View File

@ -10,8 +10,10 @@
display: flex;
flex-direction: column;
font-size: 14px;
user-select: none;
> div{
transition: all 100ms linear;
cursor: pointer;
display: flex;
@ -26,4 +28,8 @@
background-color: #e3e3e3;
color: rgba(36, 36, 36, 0.7);
}
> div:active{
transform: scale(0.98);
filter: brightness(110%);
}
}

View File

@ -18,27 +18,26 @@ export default class ContextMenu extends React.Component<ContextMenu_props>{
this.setWrapperRef = this.setWrapperRef.bind(this)
this.handleClickOutside = this.handleClickOutside.bind(this)
this.eventListener = document.addEventListener('click', this.handleClickOutside, false)
this.eventListener = () => {
document.addEventListener('click', this.handleClickOutside, false)
this.listening = true
}
}
setWrapperRef(node){
this.wrapperRef = node
}
handleClickOutside(event) {
if ( this.props.visible || this.wrapperRef && !this.wrapperRef.contains(event.target)) {
window.contextMenu.toogle()
if ( this.props.visible && this.wrapperRef && !this.wrapperRef.contains(event.target)) {
this.listening = false
window.contextMenu.toogle()
document.removeEventListener('click', this.eventListener, false)
}
}
componentDidUpdate(){
if (!this.listening) {
this.listening = true
this.eventListener
}
!this.listening ? this.eventListener() : null
}
render(){

View File

@ -9,10 +9,8 @@ import classnames from 'classnames'
import settings from 'core/libs/settings'
import { router } from 'core/cores'
import { notify } from 'core/libs/interface'
import LikeBtn from './components/like'
import { connect } from 'umi'
import config from 'config'
const { Meta } = antd.Card
@ -33,6 +31,44 @@ const defaultPayload = {
ReportIgnore: false,
}
const contextMenuPost = [
{
key: "inspect_element",
title: "Copy URL",
icon: <Icons.Clipboard />,
params: {
onClick: (e) => {
core.writeToClipboard(core.generatePostURI(e.id))
}
}
},
{
key: "screenshot",
title: "Save screenshot",
icon: <Icons.Aperture />,
params: {
itemProps: {
style: { color: "#40a9ff" }
},
onClick: (e) => {
core.createScreenshotFromElement(document.getElementById(e.id))
}
}
},
{
key: "require_test",
title: "Require Test => DEV",
icon: <Icons.Cloud />,
params: {
onClick: (e) => {
console.log('Heeeey you developeeer')
},
keepOnClick: true,
require: "dev"
}
}
]
@connect(({ app }) => ({ app }))
export default class PostCard extends React.Component {
state = {
@ -47,13 +83,6 @@ export default class PostCard extends React.Component {
})
}
generatePostURI(id){
if(config.app_config.endpoint_global && id){
return `${config.app_config.endpoint_global}/post/${id}`
}
return null
}
goElementById(id){
document.getElementById(id).scrollIntoView({
behavior: "smooth",
@ -66,19 +95,6 @@ export default class PostCard extends React.Component {
this.setState({ visibleMoreMenu: !this.state.visibleMoreMenu })
}
handleActions(){
}
writeToClipboard(text){
navigator.clipboard.writeText(text)
.then(() => {
notify.info('Copy to clipboard')
}, () => {
/* failure */
})
}
renderReportedPost(){
if(this.state.ReportIgnore) return null
return (
@ -160,6 +176,8 @@ export default class PostCard extends React.Component {
}
render() {
//
const actions = [
<LikeBtn count={this.state.payload.post_likes} liked={core.booleanFix(this.state.payload.is_liked)} />,
<Icons.Share2 />,
@ -169,12 +187,12 @@ export default class PostCard extends React.Component {
]
return (
<div className={styles.post_card_wrapper}>
<div key={this.state.payload.id} id={this.state.payload.id} className={styles.post_card_wrapper}>
<antd.Card
className={settings("post_hidebar") ? null : styles.showMode}
onDoubleClick={() => null}
onClick={() => this.goElementById(this.state.payload.id)}
onContextMenu={() => this.writeToClipboard(this.generatePostURI(this.state.payload.id))}
onContextMenu={(e) => { window.contextMenu.open({ xPos: e.clientX, yPos: e.clientY, fragment: window.contextMenu.generate(contextMenuPost, this.state.payload) }) }}
actions={actions}
hoverable
>

View File

@ -6,14 +6,14 @@ import { i18n, app_config } from 'config';
import * as errorHandlers from 'core/libs/errorhandler'
import platform from 'platform'
import request from 'request'
import html2canvas from 'html2canvas'
const { pathToRegexp } = require('path-to-regexp');
export const languages = i18n ? i18n.languages.map(item => item.key) : [];
export const defaultLanguage = i18n ? i18n.defaultLanguage : '';
import './libs'
import './cores'
import * as libs from './libs'
export const package_json = require('../../package.json');
export const UUAID = `${package_json.name}==${package_json.UUID}`;
@ -29,6 +29,35 @@ export const app_info = {
layout: platform.layout
};
export function createScreenshotFromElement(element){
if (!element) return false
html2canvas(element, {
useCORS: true,
proxy: app_config.proxy_local,
scale: 4,
backgroundColor: "transparent"
}).then(canvas => {
downloadEncodedURI({ data: canvas.toDataURL() })
})
}
export function generatePostURI(id){
if(app_config.endpoint_global && id){
return `${app_config.endpoint_global}/post/${id}`
}
return null
}
export function writeToClipboard(text){
navigator.clipboard.writeText(text)
.then(() => {
libs.Interface.notify.info('Copy to clipboard')
}, () => {
/* failure */
})
}
// [Experimental], not in use
export function getGlobals(params, callback) {
if (!params || !params.server) return false
@ -79,12 +108,32 @@ export function urlToBase64(url, callback){
xhr.send();
}
export function b64toBlob(b64Data, contentType='', sliceSize=512){
const byteCharacters = atob(b64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, {type: contentType});
return blob;
}
/**
* Generate a download with encoded uri
*
* @param {object} payload - Generation Data
*/
export function downloadEncodedURI(payload){
export function downloadDecodedURI(payload){
if(!payload) return false
let { data, type, charset, filename } = payload
/**
@ -94,17 +143,37 @@ export function downloadEncodedURI(payload){
if (!data || !type) return false
try {
if (!filename) {
filename = `export_${time.now()}.${type.split("/")[1]}`
filename = `${app_config.id}_${time.now()}.${type.split("/")[1]}`
}
let tmp = document.createElement('a')
tmp.href = `data:${type};charset=${charset},${encodeURIComponent(data)}`
tmp.download=filename
tmp.download= filename
tmp.click()
} catch (error) {
errorHandlers.onError.internal_proccess(error)
}
}
export function downloadEncodedURI(payload){
if(!payload) return false
let { data, filename } = payload
/**
*
* @param {object} payload - Generation Data
*/
if (!data) return false
try {
if (!filename) {
filename = `${app_config.id}_${time.now()}.${data.split("/")[1].split(";")[0]}`
}
let tmp = document.createElement('a')
tmp.href = data
tmp.download= filename
tmp.click()
} catch (error) {
errorHandlers.onError.internal_proccess(error)
}
}
/**
* Return the last object from array
*

View File

@ -1,7 +1,7 @@
import * as React from 'react'
import * as antd from 'antd'
import * as Icons from 'components/Icons'
import { downloadEncodedURI } from 'core'
import { downloadDecodedURI } from 'core'
export interface exportData_props {
data: string;
@ -25,7 +25,7 @@ const exportData_render = (props: exportData_props) => {
antd.Modal.confirm({
title: <div><Icons.Code /> Your export <antd.Tag> {`${props.type.split("/")[1]}`} </antd.Tag></div>,
icon: null,
onOk: () => downloadEncodedURI({data: props.data, type: props.type}),
onOk: () => downloadDecodedURI({data: props.data, type: props.type}),
okText: <><Icons.Download />Download as File</> ,
cancelText: "Done",
content: exportCodeRender(props.data),

View File

@ -13,22 +13,12 @@ import classnames from 'classnames'
import { app_config } from 'config'
import { theme } from 'core/libs/style'
import * as antd from 'antd'
import * as Icons from 'components/Icons'
import contextMenuList from 'globals/contextMenu'
import styles from './PrimaryLayout.less'
const contextMenuList = [
{
key: "inspect_element",
title: "Inspect",
icon: <Icons.Command />
}
]
const { Content } = antd.Layout
const { Sider, Overlay, ContextMenu } = AppLayout
const isActive = (key) => { return key? key.active : false }
const currentTheme = theme.get()
@withRouter
@connect(({ app, loading }) => ({ app, loading }))
@ -39,53 +29,90 @@ class PrimaryLayout extends React.Component {
collapsed: app_config.default_collapse_sider ? true : false,
isMobile: false
},
this.handleContextMenu = window.addEventListener("contextmenu", (e) => {
this.handleContextMenu = document.getElementById("root").addEventListener("contextmenu", (e) => {
e.preventDefault()
window.contextMenu.open({ xPos: e.clientX, yPos: e.clientY, fragment: this.generateContextMenu() })
},false )
window.DarkMode = isActive(currentTheme["darkmode"])? true : false
window.contextMenu.open({ xPos: e.clientX, yPos: e.clientY, fragment: window.contextMenu.generate(contextMenuList, e) })
}, false)
// include API extensions
window.requireQuery = (require) =>{
return new Promise(resolve => {
this.props.dispatch({
type: 'app/isUser',
payload: require,
callback: (e) => {
resolve(e)
}
})
})
}
window.inspectElement = (e) => this.props.dispatch({
type: "app/ipcInvoke",
payload: {
key: "inspectElement",
payload: { x: e.clientX, y: e.clientY }
}
})
window.contextMenu = this.props.app.contextMenu
window.contextMenu.open = (payload) => {
if (!payload) return false
this.props.dispatch({
type: "app/updateState",
payload: {contextMenu: {
...this.props.app.contextMenu,
xPos: payload.xPos,
yPos: payload.yPos,
fragment: payload.fragment,
visible: true
}}
})
}
window.contextMenu.handle = (e, ...rest) => {
if(!e || typeof(e) == 'undefined') {
return false
}
typeof(e.onClick) !== 'undefined' && e.onClick ? e.onClick(...rest) : null
typeof(e.keepOnClick) !== 'undefined' && e.keepOnClick ? null : window.contextMenu.toogle()
}
window.contextMenu.generate = (payload, ...rest) => {
if(!payload) return false
let tmp = []
payload.forEach(async(e) => {
if (typeof(e.params.require) !== 'undefined') {
if(await window.requireQuery(e.params.require)){
e.valid = true
tmp.push(e)
}else{
// bruh
}
}else{
tmp.push(e)
}
})
return tmp.map((e) => {
return(
<div {...e.params.itemProps} onClick={() => window.contextMenu.handle(e.params, ...rest)} key={e.key}>
{e.icon}{e.title}
</div>
)
})
}
window.contextMenu.toogle = () => {
this.props.dispatch({
type: "app/updateState",
payload: {contextMenu: {...this.props.app.contextMenu, visible: !this.props.app.contextMenu.visible} }
})
}
window.contextMenu.open = (payload) => {
if (!payload) return false
const fragment = payload.fragment || null
const xPos = payload.xPos || null
const yPos = payload.yPos || null
this.props.dispatch({
type: "app/updateState",
payload: {contextMenu: {...this.props.app.contextMenu, xPos, yPos, fragment, visible: true}}
})
}
}
handleContextMenuActions = {
inspect_element: (e) =>{
this.props.dispatch({
type: "app/ipcInvoke",
payload: {
key: "inspectElement",
payload: { x: e.clientX, y: e.clientY }
}
})
}
}
generateContextMenu() {
return contextMenuList.map((e) => {
return(
<div onClick={this.handleContextMenuActions[e.key]} key={e.key}>
{e.icon}{e.title}
</div>
)
})
}
componentDidMount() {
this.handleContextMenu
@ -116,32 +143,14 @@ class PrimaryLayout extends React.Component {
const { collapsed, isMobile } = this.state
const { onCollapseChange } = this
const { contextMenu } = app
const app_theme = isActive(currentTheme["darkmode"])? "dark" : null
const currentTheme = theme.get()
const SiderProps = { isMobile, collapsed, onCollapseChange }
const OverlayProps = { isMobile }
const breakpoint = {
xs: '480px',
sm: '576px',
md: '768px',
lg: '992px',
xl: '1200px',
xxl: '1600px',
}
window.darkMode = isActive(currentTheme["darkmode"])? true : false
document.getElementsByTagName("body")[0].setAttribute("class", window.darkMode? "dark" : "light")
const SiderProps = {
breakpoint,
isMobile,
collapsed,
onCollapseChange,
app_theme
}
const OverlayProps = {
breakpoint,
isMobile,
app_theme
}
return (
<React.Fragment>
<ContextMenu
@ -164,7 +173,10 @@ class PrimaryLayout extends React.Component {
overflow: "hidden",
opacity: currentTheme.backgroundImage.opacity
}} /> : null}
<antd.Layout id="app" className={classnames(styles.app, {[styles.interfaced]: this.props.app.electron, [styles.dark_mode]: isActive(currentTheme['darkmode']) } )}>
<antd.Layout id="app" className={classnames(styles.app, {
[styles.interfaced]: this.props.app.electron,
[styles.dark_mode]: window.darkMode
} )}>
<Sider {...SiderProps} />
<div className={styles.primary_layout_container}>
<Content

View File

@ -1,9 +1,14 @@
import React, { Component } from 'react'
import React from 'react'
import BaseLayout from './BaseLayout'
import { withRouter } from 'umi'
const appBody = document.getElementsByTagName("body")[0]
@withRouter
class Layout extends Component {
class Layout extends React.Component {
componentDidMount(){
const appBody = document.getElementsByTagName("body")[0]
appBody.setAttribute("id", "appWrapper")
}
render() {
const { children } = this.props
return (

View File

@ -87,14 +87,14 @@ export default {
},
effects: {
*query({ payload }, { call, put, select }) {
const service = yield select(state => state.app.service_valid);
const session = yield select(state => state.app.session_valid);
const service = yield select(state => state.app.service_valid)
const session = yield select(state => state.app.session_valid)
const sessionDataframe = yield select(state => state.app.session_data)
if (!service) {
console.error('❌ Cannot connect with validate session service!');
console.error('❌ Cannot connect with validate session service!')
}
if (!sessionDataframe && session ) {
console.log('Updating dataframe!')
yield put({ type: 'handleUpdateData' })
@ -141,7 +141,7 @@ export default {
let container = yield select(state => state.app.app_theme)
let style_keys = []
let tmp = []
container.forEach((e)=>{style_keys[e.key] = e.value})
if(!style_keys[payload.key]){
@ -163,7 +163,7 @@ export default {
const session = yield select(state => state.app.session_valid);
let sessionAuthframe = cookie.get(app_config.session_token_storage)
let sessionDataframe = sessionStorage.getItem(app_config.session_data_storage)
if (sessionAuthframe) {
try {
sessionAuthframe = jwt.decode(sessionAuthframe)
@ -172,18 +172,18 @@ export default {
verbosity.error('Invalid AUTHFRAME !', error)
cookie.remove(app_config.session_token_storage)
}
}
}
if (sessionDataframe) {
try {
sessionDataframe = JSON.parse(atob(sessionDataframe))
yield put({ type: 'handleUpdateDataFrames', payload: sessionDataframe })
} catch (error) {
verbosity.error('Invalid DATAFRAME !', error, session)
sessionDataframe = null
verbosity.error('Invalid DATAFRAME !', error, session)
sessionDataframe = null
sessionStorage.clear()
}
}
}
} catch (error) {
verbosity.error(error)
}
@ -212,7 +212,7 @@ export default {
if (state.session_authframe) {
if (settings("session_noexpire")) {
state.session_valid = true
return
return
}
const tokenExp = state.session_authframe.exp * 1000
const tokenExpLocale = new Date(tokenExp).toLocaleString()
@ -223,7 +223,7 @@ export default {
settings("session_noexpire") ? '( Infinite )' : `( ${tokenExpLocale} )`
} || NOW => ${now}`
)
if (tokenExp < now) {
verbosity.debug('This token is expired !!!')
state.session_valid = false
@ -251,11 +251,9 @@ export default {
isDev: sessionData.dev,
isPro: sessionData.is_pro
},
exp: settings("session_noexpire")
? 0
: Math.floor(Date.now() / 1000) + 60 * 60,
}
exp: Math.floor(Date.now() / 1000) * 120
}
jwt.sign(frame, state.server_key, (err, token) => {
if (err) {
verbosity.error(err)
@ -273,7 +271,7 @@ export default {
},
handleUpdateData(state){
const frame = {
id: state.session_uuid,
id: state.session_uuid,
access_token: state.session_token,
serverKey: state.server_key
}
@ -285,7 +283,7 @@ export default {
try {
const session_data = JSON.stringify(JSON.parse(res)["user_data"])
sessionStorage.setItem(app_config.session_data_storage, btoa(session_data))
state.session_data = session_data
location.reload()
} catch (error) {
verbosity.error(error)
}
@ -364,6 +362,8 @@ export default {
state.session_authframe = null;
cookie.remove(app_config.session_token_storage)
sessionStorage.clear()
router.push('/')
location.reload()
},
},
};

View File

@ -6,6 +6,7 @@ import { connect } from 'umi'
import settings from 'core/libs/settings'
import { PostCard, PostCreator } from 'components'
import * as antd from 'antd'
import styles from './index.less'
@connect(({ app }) => ({ app }))
export default class Explore extends React.Component {
@ -55,14 +56,12 @@ export default class Explore extends React.Component {
}
return(
<div>
<div className={styles.exploreWrapper}>
<List
//loadMore={loadMore}
dataSource={this.state.feed}
renderItem={item => (
<div id={item.id}>
<PostCard payload={item} key={item.id} />
</div>
<PostCard payload={item}/>
)}
/>
</div>

View File

@ -0,0 +1,3 @@
.exploreWrapper{
}

View File

@ -8,6 +8,9 @@ import * as Icons from 'components/Icons'
export default class Logout extends React.Component{
componentDidMount(){
if (!this.props.app.session_valid) {
return false
}
const dispatchLogout = () => this.props.dispatch({ type: "app/logout" })
antd.Modal.confirm({

View File

@ -128,8 +128,8 @@ class BackgroundImage extends ThemeConfigurator{
key: "backgroundImage",
model: { active: false, opacity: null, src: null },
textColor: this.rgbToScheme(getComputedStyle(document.getElementById("root")).color),
overlayColor: this.rgbToScheme(getComputedStyle(document.getElementById("root")).backgroundColor),
textColor: this.rgbToScheme(getComputedStyle(document.getElementById("appWrapper")).color),
overlayColor: this.rgbToScheme(getComputedStyle(document.getElementById("appWrapper")).backgroundColor),
processing: null,
customURL: '',

View File

@ -36,26 +36,20 @@
background-color: transparent;
}
#root{
background-color: @AppTheme_global_background!important;
color: @AppTheme_global_color!important;
}
.app{
&.interfaced{
height: calc(100% - @AppTheme_global_winavbar_height)!important;
}
&.dark_mode{
filter: invert(100%);
:global{
.ant-card{
background: @AppTheme_global_background_dark!important;
filter: invert(100%);
img, svg, video {
filter: invert(100%);
color: @AppTheme_global_background;
}
}
}
}
#app {
@ -64,7 +58,6 @@
position: absolute;
bottom: 0;
color: @AppTheme_global_color!important;
background-repeat: repeat-x;
background-size: cover;
background-position-y: center;
@ -89,6 +82,8 @@
}
body {
-webkit-app-region: no-drag;
@ -101,7 +96,12 @@ body {
line-height: @base-line-height;
font-family: @__Global_texted_font;
background-color: @AppTheme_global_background!important;
color: @AppTheme_global_color!important;
background-color: @AppTheme_global_background;
&.dark{
background-color: @AppTheme_global_color;
}
}
@media (max-width: @bp-small){