mirror of
https://github.com/status-im/codimd.git
synced 2025-03-01 14:20:28 +00:00
Merge branch 'develop'
This commit is contained in:
commit
fadc599fe7
16
README.md
16
README.md
@ -9,6 +9,22 @@ CodiMD
|
|||||||
CodiMD lets you collaborate in real-time with markdown.
|
CodiMD lets you collaborate in real-time with markdown.
|
||||||
Built on [HackMD](https://hackmd.io) source code, CodiMD lets you host and control your team's content with speed and ease.
|
Built on [HackMD](https://hackmd.io) source code, CodiMD lets you host and control your team's content with speed and ease.
|
||||||
|
|
||||||
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
# Table of Contents
|
||||||
|
|
||||||
|
- [CodiMD - The Open Source HackMD](#codimd---the-open-source-hackmd)
|
||||||
|
- [Documentation](#documentation)
|
||||||
|
- [Deployment](#deployment)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Upgrading and Migration](#upgrading-and-migration)
|
||||||
|
- [Developer](#developer)
|
||||||
|
- [Contribution and Discussion](#contribution-and-discussion)
|
||||||
|
- [Browser Support](#browser-support)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
## CodiMD - The Open Source HackMD
|
## CodiMD - The Open Source HackMD
|
||||||
[HackMD](https://hackmd.io) helps developers write better documents and build active communities with open collaboration.
|
[HackMD](https://hackmd.io) helps developers write better documents and build active communities with open collaboration.
|
||||||
HackMD is built with one promise - **You own and control all your content**:
|
HackMD is built with one promise - **You own and control all your content**:
|
||||||
|
@ -56,6 +56,8 @@ module.exports = {
|
|||||||
// socket.io
|
// socket.io
|
||||||
heartbeatInterval: 5000,
|
heartbeatInterval: 5000,
|
||||||
heartbeatTimeout: 10000,
|
heartbeatTimeout: 10000,
|
||||||
|
// toobusy-js
|
||||||
|
responseMaxLag: 70,
|
||||||
// document
|
// document
|
||||||
documentMaxLength: 100000,
|
documentMaxLength: 100000,
|
||||||
// image upload setting, available options are imgur/s3/filesystem/azure
|
// image upload setting, available options are imgur/s3/filesystem/azure
|
||||||
@ -66,7 +68,8 @@ module.exports = {
|
|||||||
s3: {
|
s3: {
|
||||||
accessKeyId: undefined,
|
accessKeyId: undefined,
|
||||||
secretAccessKey: undefined,
|
secretAccessKey: undefined,
|
||||||
region: undefined
|
region: undefined,
|
||||||
|
endpoint: undefined
|
||||||
},
|
},
|
||||||
minio: {
|
minio: {
|
||||||
accessKey: undefined,
|
accessKey: undefined,
|
||||||
|
@ -33,6 +33,7 @@ module.exports = {
|
|||||||
dbURL: process.env.CMD_DB_URL,
|
dbURL: process.env.CMD_DB_URL,
|
||||||
sessionSecret: process.env.CMD_SESSION_SECRET,
|
sessionSecret: process.env.CMD_SESSION_SECRET,
|
||||||
sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE),
|
sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE),
|
||||||
|
responseMaxLag: toIntegerConfig(process.env.CMD_RESPONSE_MAX_LAG),
|
||||||
imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE,
|
imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE,
|
||||||
imgur: {
|
imgur: {
|
||||||
clientID: process.env.CMD_IMGUR_CLIENTID
|
clientID: process.env.CMD_IMGUR_CLIENTID
|
||||||
@ -40,7 +41,8 @@ module.exports = {
|
|||||||
s3: {
|
s3: {
|
||||||
accessKeyId: process.env.CMD_S3_ACCESS_KEY_ID,
|
accessKeyId: process.env.CMD_S3_ACCESS_KEY_ID,
|
||||||
secretAccessKey: process.env.CMD_S3_SECRET_ACCESS_KEY,
|
secretAccessKey: process.env.CMD_S3_SECRET_ACCESS_KEY,
|
||||||
region: process.env.CMD_S3_REGION
|
region: process.env.CMD_S3_REGION,
|
||||||
|
endpoint: process.env.CMD_S3_ENDPOINT
|
||||||
},
|
},
|
||||||
minio: {
|
minio: {
|
||||||
accessKey: process.env.CMD_MINIO_ACCESS_KEY,
|
accessKey: process.env.CMD_MINIO_ACCESS_KEY,
|
||||||
|
@ -28,6 +28,7 @@ module.exports = {
|
|||||||
dbURL: process.env.HMD_DB_URL,
|
dbURL: process.env.HMD_DB_URL,
|
||||||
sessionSecret: process.env.HMD_SESSION_SECRET,
|
sessionSecret: process.env.HMD_SESSION_SECRET,
|
||||||
sessionLife: toIntegerConfig(process.env.HMD_SESSION_LIFE),
|
sessionLife: toIntegerConfig(process.env.HMD_SESSION_LIFE),
|
||||||
|
responseMaxLag: toIntegerConfig(process.env.HMD_RESPONSE_MAX_LAG),
|
||||||
imageUploadType: process.env.HMD_IMAGE_UPLOAD_TYPE,
|
imageUploadType: process.env.HMD_IMAGE_UPLOAD_TYPE,
|
||||||
imgur: {
|
imgur: {
|
||||||
clientID: process.env.HMD_IMGUR_CLIENTID
|
clientID: process.env.HMD_IMGUR_CLIENTID
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
module.exports = {
|
module.exports = {
|
||||||
up: function (queryInterface, Sequelize) {
|
up: async function (queryInterface, Sequelize) {
|
||||||
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') })
|
await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') })
|
||||||
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') })
|
await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') })
|
||||||
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') })
|
await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') })
|
||||||
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') })
|
await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') })
|
||||||
},
|
},
|
||||||
|
|
||||||
down: function (queryInterface, Sequelize) {
|
down: async function (queryInterface, Sequelize) {
|
||||||
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT })
|
await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT })
|
||||||
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT })
|
await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT })
|
||||||
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT })
|
await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT })
|
||||||
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT })
|
await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
up: function (queryInterface, Sequelize) {
|
up: async function (queryInterface, Sequelize) {
|
||||||
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') })
|
await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') })
|
||||||
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') })
|
await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') })
|
||||||
},
|
},
|
||||||
|
|
||||||
down: function (queryInterface, Sequelize) {
|
down: async function (queryInterface, Sequelize) {
|
||||||
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT })
|
await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT })
|
||||||
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT })
|
await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
up: function (queryInterface, Sequelize) {
|
up: async function (queryInterface, Sequelize) {
|
||||||
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') })
|
await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') })
|
||||||
},
|
},
|
||||||
|
|
||||||
down: function (queryInterface, Sequelize) {
|
down: async function (queryInterface, Sequelize) {
|
||||||
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') })
|
await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ var config = require('./config')
|
|||||||
var logger = require('./logger')
|
var logger = require('./logger')
|
||||||
var models = require('./models')
|
var models = require('./models')
|
||||||
var utils = require('./utils')
|
var utils = require('./utils')
|
||||||
|
var history = require('./history')
|
||||||
|
|
||||||
// public
|
// public
|
||||||
var response = {
|
var response = {
|
||||||
@ -106,6 +107,12 @@ function responseCodiMD (res, note) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateHistory (userId, note, document, time) {
|
||||||
|
var noteId = note.alias ? note.alias : models.Note.encodeNoteId(note.id)
|
||||||
|
history.updateHistory(userId, noteId, document, time)
|
||||||
|
logger.info('history updated')
|
||||||
|
}
|
||||||
|
|
||||||
function newNote (req, res, next) {
|
function newNote (req, res, next) {
|
||||||
var owner = null
|
var owner = null
|
||||||
var body = ''
|
var body = ''
|
||||||
@ -125,6 +132,10 @@ function newNote (req, res, next) {
|
|||||||
alias: req.alias ? req.alias : null,
|
alias: req.alias ? req.alias : null,
|
||||||
content: body
|
content: body
|
||||||
}).then(function (note) {
|
}).then(function (note) {
|
||||||
|
if (req.isAuthenticated()) {
|
||||||
|
updateHistory(owner, note, body)
|
||||||
|
}
|
||||||
|
|
||||||
return res.redirect(config.serverURL + '/' + models.Note.encodeNoteId(note.id))
|
return res.redirect(config.serverURL + '/' + models.Note.encodeNoteId(note.id))
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
require('babel-polyfill')
|
require('babel-polyfill')
|
||||||
require('isomorphic-fetch');
|
require('isomorphic-fetch')
|
||||||
const Router = require('express').Router
|
const Router = require('express').Router
|
||||||
const passport = require('passport')
|
const passport = require('passport')
|
||||||
const MattermostClient = require('mattermost-redux/client/client4').default
|
const MattermostClient = require('mattermost-redux/client/client4').default
|
||||||
|
@ -16,5 +16,12 @@ exports.uploadImage = function (imagePath, callback) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null, (new URL(path.basename(imagePath), config.serverURL + '/uploads/')).href)
|
let url
|
||||||
|
try {
|
||||||
|
url = (new URL(path.basename(imagePath), config.serverURL + '/uploads/')).href
|
||||||
|
} catch (e) {
|
||||||
|
url = config.serverURL + '/uploads/' + path.basename(imagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, url)
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,9 @@ exports.uploadImage = function (imagePath, callback) {
|
|||||||
callback(new Error(err), null)
|
callback(new Error(err), null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
callback(null, `${protocol}://${config.minio.endPoint}:${config.minio.port}/${config.s3bucket}/${key}`)
|
let hidePort = [80, 443].includes(config.minio.port)
|
||||||
|
let urlPort = hidePort ? '' : `:${config.minio.port}`
|
||||||
|
callback(null, `${protocol}://${config.minio.endPoint}${urlPort}/${config.s3bucket}/${key}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,9 @@ exports.uploadImage = function (imagePath, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let s3Endpoint = 's3.amazonaws.com'
|
let s3Endpoint = 's3.amazonaws.com'
|
||||||
if (config.s3.region && config.s3.region !== 'us-east-1') {
|
if (config.s3.endpoint) {
|
||||||
|
s3Endpoint = config.s3.endpoint
|
||||||
|
} else if (config.s3.region && config.s3.region !== 'us-east-1') {
|
||||||
s3Endpoint = `s3-${config.s3.region}.amazonaws.com`
|
s3Endpoint = `s3-${config.s3.region}.amazonaws.com`
|
||||||
}
|
}
|
||||||
callback(null, `https://${s3Endpoint}/${config.s3bucket}/${params.Key}`)
|
callback(null, `https://${s3Endpoint}/${config.s3bucket}/${params.Key}`)
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
const toobusy = require('toobusy-js')
|
const toobusy = require('toobusy-js')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
const response = require('../../response')
|
const response = require('../../response')
|
||||||
|
|
||||||
|
toobusy.maxLag(config.responseMaxLag);
|
||||||
|
|
||||||
module.exports = function (req, res, next) {
|
module.exports = function (req, res, next) {
|
||||||
if (toobusy()) {
|
if (toobusy()) {
|
||||||
response.errorServiceUnavailable(res)
|
response.errorServiceUnavailable(res)
|
||||||
|
@ -33,7 +33,7 @@ UserRouter.get('/me', function (req, res) {
|
|||||||
return response.errorInternalError(res)
|
return response.errorInternalError(res)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
res.send({
|
res.status(401).send({
|
||||||
status: 'forbidden'
|
status: 'forbidden'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,42 @@
|
|||||||
{
|
{
|
||||||
"Collaborative markdown notes": "Markdown 協作筆記",
|
"Collaborative markdown notes": "Markdown 協作筆記",
|
||||||
"Realtime collaborative markdown notes on all platforms.": "使用 Markdown 的跨平台即時協作筆記",
|
"Realtime collaborative markdown notes on all platforms.": "使用 Markdown 的跨平台即時協作筆記",
|
||||||
"Best way to write and share your knowledge in markdown.": "您使用 Markdown 寫作與分享知識的最佳方式",
|
"Best way to write and share your knowledge in markdown.": "使用 Markdown 寫作與分享知識的最佳方式",
|
||||||
"Intro": "簡介",
|
"Intro": "簡介",
|
||||||
"History": "紀錄",
|
"History": "歷史記錄",
|
||||||
"New guest note": "建立訪客筆記",
|
"New guest note": "建立訪客筆記",
|
||||||
"Collaborate with URL": "使用網址協作",
|
"Collaborate with URL": "使用網址協作",
|
||||||
"Support charts and MathJax": "支援圖表與 MathJax",
|
"Support charts and MathJax": "支援圖表與 MathJax",
|
||||||
"Support slide mode": "支援簡報模式",
|
"Support slide mode": "支援簡報模式",
|
||||||
"Sign In": "登入",
|
"Sign In": "登入",
|
||||||
"Below is the history from browser": "以下為來自瀏覽器的紀錄",
|
"Below is the history from browser": "以下是從瀏覽器取得的歷史記錄",
|
||||||
"Welcome!": "歡迎!",
|
"Welcome!": "歡迎!",
|
||||||
"New note": "建立筆記",
|
"New note": "建立筆記",
|
||||||
"or": "或",
|
"or": "或",
|
||||||
"Sign Out": "登出",
|
"Sign Out": "登出",
|
||||||
"Explore all features": "探索所有功能",
|
"Explore all features": "探索所有功能",
|
||||||
"Select tags...": "選擇標籤...",
|
"Select tags...": "選擇標籤…",
|
||||||
"Search keyword...": "搜尋關鍵字...",
|
"Search keyword...": "搜尋關鍵字…",
|
||||||
"Sort by title": "用標題排序",
|
"Sort by title": "依標題排序",
|
||||||
"Title": "標題",
|
"Title": "標題",
|
||||||
"Sort by time": "用時間排序",
|
"Sort by time": "依時間排序",
|
||||||
"Time": "時間",
|
"Time": "時間",
|
||||||
"Export history": "匯出紀錄",
|
"Export history": "匯出記錄",
|
||||||
"Import history": "匯入紀錄",
|
"Import history": "匯入記錄",
|
||||||
"Clear history": "清空紀錄",
|
"Clear history": "清空記錄",
|
||||||
"Refresh history": "更新紀錄",
|
"Refresh history": "重新整理記錄",
|
||||||
"No history": "沒有紀錄",
|
"No history": "沒有記錄",
|
||||||
"Import from browser": "從瀏覽器匯入",
|
"Import from browser": "從瀏覽器匯入",
|
||||||
"Releases": "版本",
|
"Releases": "版本",
|
||||||
"Are you sure?": "你確定嗎?",
|
"Are you sure?": "您確定嗎?",
|
||||||
"Do you really want to delete this note?": "確定要刪除這個文件嗎?",
|
"Do you really want to delete this note?": "確定刪除這則筆記?",
|
||||||
"All users will lose their connection.": "所有使用者將會失去連線",
|
"All users will lose their connection.": "所有使用者將會失去連線",
|
||||||
"Cancel": "取消",
|
"Cancel": "取消",
|
||||||
"Yes, do it!": "沒錯,就這樣辦!",
|
"Yes, do it!": "沒錯,就這麼做!",
|
||||||
"Choose method": "選擇方式",
|
"Choose method": "選擇方式",
|
||||||
"Sign in via %s": "透過 %s 登入",
|
"Sign in via %s": "透過 %s 登入",
|
||||||
"New": "新增",
|
"New": "新增",
|
||||||
"Publish": "發表",
|
"Publish": "發佈",
|
||||||
"Extra": "增益",
|
"Extra": "增益",
|
||||||
"Revision": "修訂版本",
|
"Revision": "修訂版本",
|
||||||
"Slide Mode": "簡報模式",
|
"Slide Mode": "簡報模式",
|
||||||
@ -55,7 +55,7 @@
|
|||||||
"You have an incompatible client version.": "您使用的是不相容的客戶端",
|
"You have an incompatible client version.": "您使用的是不相容的客戶端",
|
||||||
"Refresh to update.": "請重新整理來更新",
|
"Refresh to update.": "請重新整理來更新",
|
||||||
"New version available!": "新版本來了!",
|
"New version available!": "新版本來了!",
|
||||||
"See releases notes here": "請由此查閱更新紀錄",
|
"See releases notes here": "請由此查閱更新記錄",
|
||||||
"Refresh to enjoy new features.": "請重新整理來享受最新功能",
|
"Refresh to enjoy new features.": "請重新整理來享受最新功能",
|
||||||
"Your user state has changed.": "您的使用者狀態已變更",
|
"Your user state has changed.": "您的使用者狀態已變更",
|
||||||
"Refresh to load new user state.": "請重新整理來載入新的使用者狀態",
|
"Refresh to load new user state.": "請重新整理來載入新的使用者狀態",
|
||||||
@ -66,7 +66,7 @@
|
|||||||
"Send us email": "寄信給我們",
|
"Send us email": "寄信給我們",
|
||||||
"Documents": "文件",
|
"Documents": "文件",
|
||||||
"Features": "功能簡介",
|
"Features": "功能簡介",
|
||||||
"YAML Metadata": "YAML Metadata",
|
"YAML Metadata": "YAML 中繼資料",
|
||||||
"Slide Example": "簡報範例",
|
"Slide Example": "簡報範例",
|
||||||
"Cheatsheet": "快速簡表",
|
"Cheatsheet": "快速簡表",
|
||||||
"Example": "範例",
|
"Example": "範例",
|
||||||
@ -88,7 +88,7 @@
|
|||||||
"This is a alert area.": "這是警告區塊",
|
"This is a alert area.": "這是警告區塊",
|
||||||
"Revert": "還原",
|
"Revert": "還原",
|
||||||
"Import from clipboard": "從剪貼簿匯入",
|
"Import from clipboard": "從剪貼簿匯入",
|
||||||
"Paste your markdown or webpage here...": "在這裡貼上 Markdown 或是網頁內容...",
|
"Paste your markdown or webpage here...": "在這裡貼上 Markdown 或是網頁內容…",
|
||||||
"Clear": "清除",
|
"Clear": "清除",
|
||||||
"This note is locked": "此份筆記已被鎖定",
|
"This note is locked": "此份筆記已被鎖定",
|
||||||
"Sorry, only owner can edit this note.": "抱歉,只有擁有者可以編輯此筆記",
|
"Sorry, only owner can edit this note.": "抱歉,只有擁有者可以編輯此筆記",
|
||||||
@ -97,23 +97,23 @@
|
|||||||
"Sorry, you've reached the max length this note can be.": "抱歉,您已使用到此份筆記可用的最大長度",
|
"Sorry, you've reached the max length this note can be.": "抱歉,您已使用到此份筆記可用的最大長度",
|
||||||
"Please reduce the content or divide it to more notes, thank you!": "請減少內容或是將內容切成更多筆記,謝謝!",
|
"Please reduce the content or divide it to more notes, thank you!": "請減少內容或是將內容切成更多筆記,謝謝!",
|
||||||
"Import from Gist": "從 Gist 匯入",
|
"Import from Gist": "從 Gist 匯入",
|
||||||
"Paste your gist url here...": "在這裡貼上 gist 網址...",
|
"Paste your gist url here...": "在此處貼上 gist 網址…",
|
||||||
"Import from Snippet": "從 Snippet 匯入",
|
"Import from Snippet": "從 Snippet 匯入",
|
||||||
"Select From Available Projects": "從可用的專案中選擇",
|
"Select From Available Projects": "從可用專案選擇",
|
||||||
"Select From Available Snippets": "從可用的 Snippets 中選擇",
|
"Select From Available Snippets": "從可用的 Snippets 選擇",
|
||||||
"OR": "或是",
|
"OR": "或是",
|
||||||
"Export to Snippet": "匯出到 Snippet",
|
"Export to Snippet": "匯出至 Snippet",
|
||||||
"Select Visibility Level": "選擇可見層級",
|
"Select Visibility Level": "選擇可見層級",
|
||||||
"Night Theme": "夜間主題",
|
"Night Theme": "夜間主題",
|
||||||
"Follow us on %s and %s.": "來 %s 或 %s 和我們互動吧!",
|
"Follow us on %s and %s.": "來 %s 或 %s 和我們互動吧!",
|
||||||
"Privacy": "隱私權政策",
|
"Privacy": "隱私權政策",
|
||||||
"Terms of Use": "使用條款",
|
"Terms of Use": "使用條款",
|
||||||
"Do you really want to delete your user account?": "你確定真的想要刪除帳戶?",
|
"Do you really want to delete your user account?": "你確定真的想要刪除帳戶?",
|
||||||
"This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "我們將會刪除你的帳戶、你所擁有的筆記、以及你在別人筆記裡的作者紀錄。",
|
"This will delete your account, all notes that are owned by you and remove all references to your account from other notes.": "我們將會刪除你的帳戶、你所擁有的筆記、以及你在別人筆記裡的作者記錄。",
|
||||||
"Delete user": "刪除使用者",
|
"Delete user": "刪除使用者",
|
||||||
"Export user data": "匯出使用者資料",
|
"Export user data": "匯出使用者資料",
|
||||||
"Help us translating on %s": "來 %s 幫我們翻譯",
|
"Help us translating on %s": "來 %s 幫我們翻譯",
|
||||||
"Source Code": "原始碼",
|
"Source Code": "原始碼",
|
||||||
"Powered by %s": "Powered by %s",
|
"Powered by %s": "技術支援:%s",
|
||||||
"Register": "註冊"
|
"Register": "註冊"
|
||||||
}
|
}
|
@ -26,7 +26,7 @@
|
|||||||
"test": "npm run-script eslint && npm run-script jsonlint && mocha"
|
"test": "npm run-script eslint && npm run-script jsonlint && mocha"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hackmd/codemirror": "~5.41.2",
|
"@hackmd/codemirror": "^5.46.2",
|
||||||
"@hackmd/diff-match-patch": "~1.1.1",
|
"@hackmd/diff-match-patch": "~1.1.1",
|
||||||
"@hackmd/idle-js": "~1.0.1",
|
"@hackmd/idle-js": "~1.0.1",
|
||||||
"@hackmd/imgur": "~0.4.1",
|
"@hackmd/imgur": "~0.4.1",
|
||||||
@ -55,7 +55,7 @@
|
|||||||
"express": "~4.16.4",
|
"express": "~4.16.4",
|
||||||
"express-session": "~1.16.1",
|
"express-session": "~1.16.1",
|
||||||
"file-saver": "~1.3.3",
|
"file-saver": "~1.3.3",
|
||||||
"flowchart.js": "~1.6.4",
|
"flowchart.js": "~1.12.0",
|
||||||
"fork-awesome": "~1.1.3",
|
"fork-awesome": "~1.1.3",
|
||||||
"formidable": "~1.2.1",
|
"formidable": "~1.2.1",
|
||||||
"gist-embed": "~2.6.0",
|
"gist-embed": "~2.6.0",
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
var ElapsedTimeBar = {
|
||||||
|
// default value
|
||||||
|
barColor: 'rgb(200,0,0)',
|
||||||
|
pausedBarColor: 'rgba(200,0,0,.6)',
|
||||||
|
|
||||||
|
isPaused: false,
|
||||||
|
isFinished: false,
|
||||||
|
|
||||||
|
allottedTime: null,
|
||||||
|
timeProgressBar: null,
|
||||||
|
startTime: null,
|
||||||
|
pauseTime: null,
|
||||||
|
pauseTimeDuration: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* initialize elements
|
||||||
|
*/
|
||||||
|
handleReady() {
|
||||||
|
var config = Reveal.getConfig();
|
||||||
|
|
||||||
|
// activate this plugin if config.allottedTime exists.
|
||||||
|
if (!config.allottedTime) {
|
||||||
|
console.warn('Failed to start ElapsedTimeBar plugin. "allottedTime" property is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set configurations
|
||||||
|
this.barColor = config.barColor || this.barColor;
|
||||||
|
this.pausedBarColor = config.pausedBarColor || this.pausedBarColor;
|
||||||
|
|
||||||
|
// calc barHeight from config.barHeight or page-progress container
|
||||||
|
var barHeight;
|
||||||
|
var pageProgressContainer = document.querySelector('.progress');
|
||||||
|
if (config.progressBarHeight) {
|
||||||
|
barHeight = parseInt(config.progressBarHeight, 10) + 'px';
|
||||||
|
|
||||||
|
// override height of page-progress container
|
||||||
|
pageProgressContainer && (pageProgressContainer.style.height = barHeight);
|
||||||
|
} else if (config.progress && pageProgressContainer) {
|
||||||
|
// get height from page-progress container
|
||||||
|
barHeight = pageProgressContainer.getBoundingClientRect().height + 'px';
|
||||||
|
} else {
|
||||||
|
// default
|
||||||
|
barHeight = '3px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// create container of time-progress
|
||||||
|
var timeProgressContainer = document.createElement('div');
|
||||||
|
timeProgressContainer.classList.add('progress');
|
||||||
|
Object.entries({
|
||||||
|
display: 'block',
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: config.progress ? barHeight : 0,
|
||||||
|
width: '100%',
|
||||||
|
height: barHeight
|
||||||
|
}).forEach(([k, v]) => {
|
||||||
|
timeProgressContainer.style[k] = v;
|
||||||
|
});
|
||||||
|
document.querySelector('.reveal').appendChild(timeProgressContainer);
|
||||||
|
|
||||||
|
// create content of time-progress
|
||||||
|
this.timeProgressBar = document.createElement('div');
|
||||||
|
Object.entries({
|
||||||
|
height: '100%',
|
||||||
|
willChange: 'width'
|
||||||
|
}).forEach(([k, v]) => {
|
||||||
|
this.timeProgressBar.style[k] = v;
|
||||||
|
});
|
||||||
|
timeProgressContainer.appendChild(this.timeProgressBar);
|
||||||
|
|
||||||
|
// start timer
|
||||||
|
this.start(config.allottedTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update repeatedly using requestAnimationFrame.
|
||||||
|
*/
|
||||||
|
loop() {
|
||||||
|
if (this.isPaused) return;
|
||||||
|
var now = +new Date();
|
||||||
|
var elapsedTime = now - this.startTime - this.pauseTimeDuration;
|
||||||
|
if (elapsedTime > this.allottedTime) {
|
||||||
|
this.timeProgressBar.style.width = '100%';
|
||||||
|
this.isFinished = true;
|
||||||
|
} else {
|
||||||
|
this.timeProgressBar.style.width = elapsedTime / this.allottedTime * 100 + '%';
|
||||||
|
requestAnimationFrame(this.loop.bind(this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set color of progress bar
|
||||||
|
*/
|
||||||
|
setBarColor() {
|
||||||
|
if (this.isPaused) {
|
||||||
|
this.timeProgressBar.style.backgroundColor = this.pausedBarColor;
|
||||||
|
} else {
|
||||||
|
this.timeProgressBar.style.backgroundColor = this.barColor;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start(reset) timer with new allotted time.
|
||||||
|
* @param {number} allottedTime
|
||||||
|
* @param {number} [elapsedTime=0]
|
||||||
|
*/
|
||||||
|
start(allottedTime, elapsedTime = 0) {
|
||||||
|
this.isFinished = false;
|
||||||
|
this.isPaused = false;
|
||||||
|
this.allottedTime = allottedTime;
|
||||||
|
this.startTime = +new Date() - elapsedTime;
|
||||||
|
this.pauseTimeDuration = 0;
|
||||||
|
this.setBarColor();
|
||||||
|
this.loop();
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.start(this.allottedTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
if (this.isPaused) return;
|
||||||
|
this.isPaused = true;
|
||||||
|
this.pauseTime = +new Date();
|
||||||
|
this.setBarColor();
|
||||||
|
},
|
||||||
|
|
||||||
|
resume() {
|
||||||
|
if (!this.isPaused) return;
|
||||||
|
|
||||||
|
// add paused time duration
|
||||||
|
this.isPaused = false;
|
||||||
|
this.pauseTimeDuration += new Date() - this.pauseTime;
|
||||||
|
this.pauseTime = null;
|
||||||
|
this.setBarColor();
|
||||||
|
this.loop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Reveal.isReady()) {
|
||||||
|
ElapsedTimeBar.handleReady();
|
||||||
|
} else {
|
||||||
|
Reveal.addEventListener('ready', () => ElapsedTimeBar.handleReady());
|
||||||
|
}
|
283
public/build/reveal.js/plugin/spotlight/spotlight.js
Normal file
283
public/build/reveal.js/plugin/spotlight/spotlight.js
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
var RevealSpotlight = window.RevealSpotlight || (function () {
|
||||||
|
|
||||||
|
//configs
|
||||||
|
var spotlightSize;
|
||||||
|
var toggleOnMouseDown;
|
||||||
|
var spotlightOnKeyPressAndHold;
|
||||||
|
var presentingCursor;
|
||||||
|
var spotlightCursor;
|
||||||
|
var initialPresentationMode;
|
||||||
|
var disablingUserSelect;
|
||||||
|
var fadeInAndOut;
|
||||||
|
var style;
|
||||||
|
var lockPointerInsideCanvas;
|
||||||
|
var getMousePos;
|
||||||
|
|
||||||
|
var drawBoard;
|
||||||
|
var isSpotlightOn = true;
|
||||||
|
var isCursorOn = true;
|
||||||
|
|
||||||
|
var lastMouseMoveEvent;
|
||||||
|
|
||||||
|
function onRevealJsReady(event) {
|
||||||
|
configure();
|
||||||
|
drawBoard = setupCanvas();
|
||||||
|
|
||||||
|
addWindowResizeListener();
|
||||||
|
|
||||||
|
addMouseMoveListener();
|
||||||
|
|
||||||
|
if (toggleOnMouseDown) {
|
||||||
|
addMouseToggleSpotlightListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spotlightOnKeyPressAndHold) {
|
||||||
|
addKeyPressAndHoldSpotlightListener(spotlightOnKeyPressAndHold);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCursor(!initialPresentationMode);
|
||||||
|
setSpotlight(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configure() {
|
||||||
|
var config = Reveal.getConfig().spotlight || {};
|
||||||
|
spotlightSize = config.size || 60;
|
||||||
|
presentingCursor = config.presentingCursor || "none";
|
||||||
|
spotlightCursor = config.spotlightCursor || "none";
|
||||||
|
var useAsPointer = config.useAsPointer || false;
|
||||||
|
var pointerColor = config.pointerColor || 'red';
|
||||||
|
lockPointerInsideCanvas = config.lockPointerInsideCanvas || false;
|
||||||
|
|
||||||
|
if(lockPointerInsideCanvas){
|
||||||
|
getMousePos = getMousePosByMovement;
|
||||||
|
} else {
|
||||||
|
getMousePos = getMousePosByBoundingClientRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If using as pointer draw a transparent background and
|
||||||
|
// the mouse pointer in the specified color or default
|
||||||
|
var pointerStyle = {
|
||||||
|
backgroundFillStyle : "rgba(0, 0, 0, 0)",
|
||||||
|
mouseFillStyle : pointerColor
|
||||||
|
};
|
||||||
|
|
||||||
|
var spotlightStyle = {
|
||||||
|
backgroundFillStyle : "#000000A8",
|
||||||
|
mouseFillStyle : "#FFFFFFFF"
|
||||||
|
};
|
||||||
|
|
||||||
|
style = useAsPointer ? pointerStyle : spotlightStyle;
|
||||||
|
|
||||||
|
if (config.hasOwnProperty("toggleSpotlightOnMouseDown")) {
|
||||||
|
toggleOnMouseDown = config.toggleSpotlightOnMouseDown;
|
||||||
|
} else {
|
||||||
|
toggleOnMouseDown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasOwnProperty("initialPresentationMode")) {
|
||||||
|
initialPresentationMode = config.initialPresentationMode;
|
||||||
|
} else {
|
||||||
|
initialPresentationMode = toggleOnMouseDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasOwnProperty("spotlightOnKeyPressAndHold")) {
|
||||||
|
spotlightOnKeyPressAndHold = config.spotlightOnKeyPressAndHold;
|
||||||
|
} else {
|
||||||
|
spotlightOnKeyPressAndHold = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasOwnProperty("disablingUserSelect")) {
|
||||||
|
disablingUserSelect = config.disablingUserSelect;
|
||||||
|
} else {
|
||||||
|
disablingUserSelect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.hasOwnProperty("fadeInAndOut")) {
|
||||||
|
fadeInAndOut = config.fadeInAndOut;
|
||||||
|
} else {
|
||||||
|
fadeInAndOut = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCanvas() {
|
||||||
|
var container = document.createElement('div');
|
||||||
|
container.id = "spotlight";
|
||||||
|
container.style.cssText = "position:absolute;top:0;left:0;bottom:0;right:0;z-index:99;";
|
||||||
|
if (fadeInAndOut) {
|
||||||
|
container.style.cssText += "transition: " + fadeInAndOut + "ms opacity;";
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
var context = canvas.getContext("2d");
|
||||||
|
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
|
||||||
|
container.appendChild(canvas);
|
||||||
|
document.body.appendChild(container);
|
||||||
|
container.style.opacity = 0;
|
||||||
|
container.style['pointer-events'] = 'none';
|
||||||
|
return {
|
||||||
|
container,
|
||||||
|
canvas,
|
||||||
|
context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addWindowResizeListener() {
|
||||||
|
window.addEventListener('resize', function (e) {
|
||||||
|
var canvas = drawBoard.canvas;
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMouseMoveListener() {
|
||||||
|
document.body.addEventListener('mousemove', function (e) {
|
||||||
|
if(isSpotlightOn) {
|
||||||
|
showSpotlight(e);
|
||||||
|
}
|
||||||
|
lastMouseMoveEvent = e;
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMouseToggleSpotlightListener() {
|
||||||
|
|
||||||
|
window.addEventListener("mousedown", function (e) {
|
||||||
|
if (!isCursorOn) {
|
||||||
|
setSpotlight(true, e);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
window.addEventListener("mouseup", function (e) {
|
||||||
|
if (!isCursorOn) {
|
||||||
|
setSpotlight(false, e);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addKeyPressAndHoldSpotlightListener(keyCode) {
|
||||||
|
|
||||||
|
window.addEventListener("keydown", function (e) {
|
||||||
|
if (!isCursorOn && !isSpotlightOn && e.keyCode === keyCode) {
|
||||||
|
setSpotlight(true, lastMouseMoveEvent);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
window.addEventListener("keyup", function (e) {
|
||||||
|
if (!isCursorOn && e.keyCode === keyCode) {
|
||||||
|
setSpotlight(false);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSpotlight() {
|
||||||
|
setSpotlight(!isSpotlightOn, lastMouseMoveEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSpotlight(isOn, mouseEvt) {
|
||||||
|
isSpotlightOn = isOn;
|
||||||
|
var container = drawBoard.container;
|
||||||
|
if (isOn) {
|
||||||
|
if (lockPointerInsideCanvas && document.pointerLockElement != drawBoard.canvas) {
|
||||||
|
drawBoard.canvas.requestPointerLock();
|
||||||
|
}
|
||||||
|
container.style.opacity = 1;
|
||||||
|
container.style['pointer-events'] = null;
|
||||||
|
document.body.style.cursor = spotlightCursor;
|
||||||
|
if (mouseEvt) {
|
||||||
|
showSpotlight(mouseEvt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.opacity = 0;
|
||||||
|
container.style['pointer-events'] = 'none';
|
||||||
|
document.body.style.cursor = presentingCursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePresentationMode() {
|
||||||
|
setCursor(!isCursorOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCursor(isOn) {
|
||||||
|
isCursorOn = isOn;
|
||||||
|
if (isOn) {
|
||||||
|
if (disablingUserSelect) {
|
||||||
|
document.body.style.userSelect = null;
|
||||||
|
}
|
||||||
|
document.body.style.cursor = null;
|
||||||
|
} else {
|
||||||
|
if (disablingUserSelect) {
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
}
|
||||||
|
document.body.style.cursor = presentingCursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSpotlight(mouseEvt) {
|
||||||
|
var canvas = drawBoard.canvas;
|
||||||
|
var context = drawBoard.context;
|
||||||
|
var mousePos = getMousePos(canvas, mouseEvt);
|
||||||
|
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Create a canvas mask
|
||||||
|
var maskCanvas = document.createElement('canvas');
|
||||||
|
maskCanvas.width = canvas.width;
|
||||||
|
maskCanvas.height = canvas.height;
|
||||||
|
|
||||||
|
var maskCtx = maskCanvas.getContext('2d');
|
||||||
|
|
||||||
|
maskCtx.fillStyle = style.backgroundFillStyle;
|
||||||
|
maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
|
||||||
|
maskCtx.globalCompositeOperation = 'xor';
|
||||||
|
|
||||||
|
maskCtx.fillStyle = style.mouseFillStyle;
|
||||||
|
maskCtx.arc(mousePos.x, mousePos.y, spotlightSize, 0, 2 * Math.PI);
|
||||||
|
maskCtx.fill();
|
||||||
|
|
||||||
|
context.drawImage(maskCanvas, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mX = 0;
|
||||||
|
var mY = 0;
|
||||||
|
|
||||||
|
function getMousePosByMovement(canvas, evt) {
|
||||||
|
var movementX = evt.movementX || 0;
|
||||||
|
var movementY = evt.movementY || 0;
|
||||||
|
mX += movementX;
|
||||||
|
mY += movementY;
|
||||||
|
|
||||||
|
if (mX > canvas.clientWidth) {
|
||||||
|
mX = canvas.clientWidth;
|
||||||
|
}
|
||||||
|
if (mY > canvas.clientHeight) {
|
||||||
|
mY = canvas.clientHeight;
|
||||||
|
}
|
||||||
|
if (mX < 0) {
|
||||||
|
mX = 0;
|
||||||
|
}
|
||||||
|
if (mY < 0) {
|
||||||
|
mY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: mX,
|
||||||
|
y: mY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMousePosByBoundingClientRect(canvas, evt) {
|
||||||
|
var rect = canvas.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: evt.clientX - rect.left,
|
||||||
|
y: evt.clientY - rect.top
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Reveal.addEventListener('ready', onRevealJsReady);
|
||||||
|
|
||||||
|
this.toggleSpotlight = toggleSpotlight;
|
||||||
|
this.togglePresentationMode = togglePresentationMode;
|
||||||
|
return this;
|
||||||
|
})();
|
@ -74,6 +74,21 @@ const defaultOptions = {
|
|||||||
const meta = JSON.parse($('#meta').text())
|
const meta = JSON.parse($('#meta').text())
|
||||||
var options = meta.slideOptions || {}
|
var options = meta.slideOptions || {}
|
||||||
|
|
||||||
|
if (options.hasOwnProperty('spotlight')) {
|
||||||
|
defaultOptions.dependencies.push({
|
||||||
|
src: `${serverurl}/build/reveal.js/plugin/spotlight/spotlight.js`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.hasOwnProperty('allottedTime') || options.hasOwnProperty('allottedMinutes')) {
|
||||||
|
defaultOptions.dependencies.push({
|
||||||
|
src: `${serverurl}/build/reveal.js/plugin/elapsed-time-bar/elapsed-time-bar.js`
|
||||||
|
})
|
||||||
|
if (options.hasOwnProperty('allottedMinutes')) {
|
||||||
|
options.allottedTime = options.allottedMinutes * 60 * 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const view = $('.reveal')
|
const view = $('.reveal')
|
||||||
|
|
||||||
// text language
|
// text language
|
||||||
|
Loading…
x
Reference in New Issue
Block a user