Merge branch 'develop'

This commit is contained in:
Etherpad Release Bot 2025-08-02 11:46:15 +00:00
commit 913d460238
75 changed files with 1755 additions and 1623 deletions

View File

@ -36,7 +36,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
@ -53,7 +53,7 @@ jobs:
run: pnpm config set auto-install-peers false
-
name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.5.0
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0
@ -96,7 +96,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
@ -113,7 +113,7 @@ jobs:
run: pnpm config set auto-install-peers false
-
name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.5.0
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0
@ -168,7 +168,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
@ -225,7 +225,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -38,7 +38,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -50,7 +50,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -36,7 +36,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -30,7 +30,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
@ -103,7 +103,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
@ -184,7 +184,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -32,7 +32,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
@ -76,7 +76,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
@ -147,7 +147,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -29,7 +29,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -32,7 +32,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -38,12 +38,12 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Only install direct dependencies
run: pnpm config set auto-install-peers false
- name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.5.0
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0
@ -62,7 +62,7 @@ jobs:
run: pnpm config set auto-install-peers false
-
name: Install libreoffice
uses: awalsh128/cache-apt-pkgs-action@v1.5.0
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
with:
packages: libreoffice libreoffice-pdfimport
version: 1.0

View File

@ -39,7 +39,7 @@ jobs:
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.0.4
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

83
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,83 @@
name: Release etherpad
on:
workflow_dispatch:
inputs:
release_type:
description: 'Choose the type of release to create'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
jobs:
releases:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
repository: ether/etherpad-lite
path: etherpad
token: '${{ secrets.ETHER_RELEASE_TOKEN }}'
fetch-depth: '0'
fetch-tags: 'true'
- name: Checkout master
working-directory: etherpad
run: |
git fetch origin master
git checkout master
git reset --hard origin/master
- name: Checkout develop
working-directory: etherpad
run: |
git fetch origin develop
git checkout develop
git reset --hard origin/develop
- name: Checkout repository
uses: actions/checkout@v4
with:
repository: ether/ether.github.com
path: ether.github.com
token: '${{ secrets.ETHER_RELEASE_TOKEN }}'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Install dependencies etherpad
run: pnpm install --frozen-lockfile
working-directory: etherpad
- name: Install dependencies ether.github.com
run: pnpm install --frozen-lockfile
working-directory: ether.github.com
- name: Set git user
run: |
git config --global user.name "Etherpad Release Bot"
git config --global user.email "noreply@etherpad.org"
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- uses: reitzig/actions-asciidoctor@v2.0.2
with:
version: 2.0.18
- name: Prepare release
working-directory: etherpad
run: |
cd bin
pnpm run release ${{ inputs.release_type }}
- name: Push after release
working-directory: etherpad
run: |
./bin/push-after-release.sh

View File

@ -1,3 +1,13 @@
# 2.4.0
### Notable enhancements and fixes
- Added home button to the pad panel. To show it in your current instance, please copy the updated "toolbar" settings from the `settings.json.template` file to your `settings.json` file.
- Added more current design for the default collibri theme.
- Added handling of recent visited pads in the collibri theme. You can now access the most recent three pads you visited in the pad panel.
- Disable stats endpoints if enableMetrics is set to false. This allows you to disable the metrics endpoints if you don't want to use them.
- Use Node LTS instead of always latest Node version.
# 2.3.2
### Notable enhancements and fixes

View File

@ -5,7 +5,7 @@
# Author: muxator
ARG BUILD_ENV=git
FROM node:alpine AS adminbuild
FROM node:lts-alpine AS adminbuild
RUN npm install -g pnpm@latest
WORKDIR /opt/etherpad-lite
COPY . .
@ -13,7 +13,7 @@ RUN pnpm install
RUN pnpm run build:ui
FROM node:alpine AS build
FROM node:lts-alpine AS build
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
# Set these arguments when building the image from behind a proxy

View File

@ -1,7 +1,7 @@
{
"name": "admin",
"private": true,
"version": "2.3.2",
"version": "2.4.0",
"type": "module",
"scripts": {
"dev": "vite",
@ -18,25 +18,25 @@
"@radix-ui/react-toast": "^1.2.14",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"@vitejs/plugin-react-swc": "^3.10.2",
"eslint": "^9.28.0",
"eslint": "^9.31.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"i18next": "^25.2.1",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.515.0",
"lucide-react": "^0.525.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.57.0",
"react-i18next": "^15.5.3",
"react-router-dom": "^7.6.2",
"react-hook-form": "^7.60.0",
"react-i18next": "^15.6.0",
"react-router-dom": "^7.6.3",
"socket.io-client": "^4.8.1",
"typescript": "^5.8.2",
"vite": "^6.3.5",
"vite-plugin-static-copy": "^3.0.0",
"vite": "^7.0.4",
"vite-plugin-static-copy": "^3.1.1",
"vite-plugin-svgr": "^4.3.0",
"zustand": "^5.0.5"
"zustand": "^5.0.6"
}
}

View File

@ -9,22 +9,29 @@ export const ShoutPage = ()=>{
const [message, setMessage] = useState<string>("");
const [sticky, setSticky] = useState<boolean>(false);
const socket = useStore(state => state.settingsSocket);
const pluginSocket = useStore(state => state.pluginsSocket);
const [shouts, setShouts] = useState<ShoutType[]>([]);
useEffect(() => {
fetch('/stats')
.then(response => response.json())
.then(data => setTotalUsers(data.totalUsers));
}, []);
useEffect(() => {
if(socket) {
if(socket && pluginSocket) {
console.log('Socket connected', socket.id);
socket.on('shout', (shout) => {
setShouts([...shouts, shout])
})
pluginSocket.on('results:stats', (statData) => {
console.log('Shoutdata', statData);
setTotalUsers(statData.totalUsers);
})
}
}, [socket, shouts])
}, [socket, shouts, pluginSocket])
useEffect(() => {
if (pluginSocket) {
pluginSocket.emit('getStats', {});
}
}, [pluginSocket]);
const sendMessage = () => {
socket?.emit('shout', {

View File

@ -28,10 +28,6 @@ export default defineConfig({
'/admin-auth/': {
target: 'http://localhost:9001',
changeOrigin: true,
},
'/stats': {
target: 'http://localhost:9001',
changeOrigin: true,
}
}
}

View File

@ -1,13 +1,13 @@
{
"name": "bin",
"version": "2.3.2",
"version": "2.4.0",
"description": "",
"main": "checkAllPads.js",
"directories": {
"doc": "doc"
},
"dependencies": {
"axios": "^1.8.4",
"axios": "^1.10.0",
"ep_etherpad-lite": "workspace:../src",
"log4js": "^6.9.1",
"semver": "^7.7.2",
@ -15,7 +15,7 @@
"ueberdb2": "^5.0.14"
},
"devDependencies": {
"@types/node": "^24.0.1",
"@types/node": "^24.0.14",
"@types/semver": "^7.7.0",
"typescript": "^5.8.2"
},

View File

@ -29,7 +29,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -18,7 +18,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -21,7 +21,7 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash

View File

@ -56,6 +56,8 @@ const readJson = (filename: string) => JSON.parse(fs.readFileSync(filename, {enc
const assertWorkDirClean = (opts:{
cwd?: string;
} = {}) => {
// Stash any changes in the working directory so that we can check for modifications.
runc('git stash')
opts.cwd = runc('git rev-parse --show-cdup', opts) || cwd;
const m = runc('git diff-files --name-status', opts);
console.log(">"+m.trim()+"<")
@ -172,8 +174,8 @@ try {
console.log('Merging develop into master...');
run('git merge --no-ff --no-edit develop');
console.log(`Creating ${newVersion} tag...`);
run(`git tag -s '${newVersion}' -m '${newVersion}'`);
run(`git tag -s 'v${newVersion}' -m 'v${newVersion}'`);
run(`git tag -a '${newVersion}' -m '${newVersion}'`);
run(`git tag -a 'v${newVersion}' -m 'v${newVersion}'`);
console.log('Switching back to develop...');
run('git checkout develop');
console.log('Merging master into develop...');

View File

@ -1,10 +1,13 @@
{
"devDependencies": {
"vitepress": "^1.6.3"
"vitepress": "^2.0.0-alpha.9"
},
"scripts": {
"docs:dev": "vitepress dev",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"peerDependencies": {
"search-insights": "^2.17.3"
}
}

View File

@ -15,5 +15,5 @@ You can choose a skin changing the parameter `skinName` in `settings.json`.
Since Etherpad **1.7.5**, two skins are included:
* `no-skin`: an empty skin, leaving the default Etherpad appearance unchanged, that you can use as a guidance to develop your own.
* `no-skin`: an empty skin, leaving the default Etherpad appearance unchanged, that you can use as guidance to develop your own.
* `colibris`: a new, experimental skin, that will become the default in Etherpad 2.0.

View File

@ -50,6 +50,6 @@
"type": "git",
"url": "https://github.com/ether/etherpad-lite.git"
},
"version": "2.3.2",
"version": "2.4.0",
"license": "Apache-2.0"
}

2364
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -171,6 +171,14 @@
*/
"showSettingsInAdminPage": "${SHOW_SETTINGS_IN_ADMIN_PAGE:true}",
/*
* Enable/disable the metrics endpoint.
*
* This is used by the monitoring plugins to collect metrics about Etherpad.
* If you do not use any monitoring plugins, you can disable this.
*/
"enableMetrics": "${ENABLE_METRICS:true}",
/*
* Settings for cleanup of pads
*/
@ -642,7 +650,7 @@
],
"right": [
["importexport", "timeslider", "savedrevision"],
["settings", "embed"],
["settings", "embed", "home"],
["showusers"]
],
"timeslider": [

View File

@ -162,6 +162,14 @@
*/
"showSettingsInAdminPage": true,
/*
* Enable/disable the metrics endpoint.
*
* This is used by the monitoring plugins to collect metrics about Etherpad.
* If you do not use any monitoring plugins, you can disable this.
*/
"enableMetrics": "${ENABLE_METRICS:true}",
/*
* Settings for cleanup of pads
*/
@ -641,7 +649,7 @@
],
"right": [
["importexport", "timeslider", "savedrevision"],
["settings", "embed"],
["settings", "embed", "home"],
["showusers"]
],
"timeslider": [

View File

@ -52,9 +52,16 @@
"admin_settings.current_save.value": "Einstellungen speichern",
"admin_settings.page-title": "Einstellungen - Etherpad",
"index.newPad": "Neues Pad",
"index.createOpenPad": "oder ein Pad mit folgendem Namen erstellen/öffnen:",
"index.createOpenPad": "Pad öffnen",
"index.openPad": "Öffne ein vorhandenes Pad mit folgendem Namen:",
"pad.toolbar.bold.title": "Fett (Strg-B)",
"index.recentPads": "Zuletzt bearbeitete Pads",
"index.recentPadsEmpty": "Keine kürzlich bearbeiteten Pads gefunden.",
"index.generateNewPad": "Neues Pad generieren",
"index.labelPad": "Padname (optional)",
"index.placeholderPadEnter": "Gib den Namen des Pads ein...",
"index.createAndShareDocuments": "Erstelle und teile Dokumente in Echtzeit",
"index.createAndShareDocumentsDescription": "Etherpad ermöglicht die gemeinsame Bearbeitung von Dokumenten in Echtzeit, ähnlich wie ein Live-Multiplayer-Editor, der in Ihrem Browser läuft.",
"pad.toolbar.bold.title": "Fett (Strg-B)",
"pad.toolbar.italic.title": "Kursiv (Strg-I)",
"pad.toolbar.underline.title": "Unterstrichen (Strg-U)",
"pad.toolbar.strikethrough.title": "Durchgestrichen (Strg+5)",
@ -70,6 +77,7 @@
"pad.toolbar.savedRevision.title": "Version speichern",
"pad.toolbar.settings.title": "Einstellungen",
"pad.toolbar.embed.title": "Dieses Pad teilen oder einbetten",
"pad.toolbar.home.title": "Zurück zur Startseite",
"pad.toolbar.showusers.title": "Benutzer dieses Pads anzeigen",
"pad.colorpicker.save": "Speichern",
"pad.colorpicker.cancel": "Abbrechen",

View File

@ -34,8 +34,16 @@
"admin_settings.page-title": "Settings - Etherpad",
"index.newPad": "New Pad",
"index.createOpenPad": "or create/open a Pad with the name:",
"index.createOpenPad": "Open pad by name",
"index.openPad": "open an existing Pad with the name:",
"index.recentPads": "Recent Pads",
"index.recentPadsEmpty": "No recent pads found.",
"index.generateNewPad": "Generate random pad name",
"index.labelPad": "Pad name (optional)",
"index.placeholderPadEnter": "Gib den Namen des Pads ein...",
"index.createAndShareDocuments": "Create and share documents in real time",
"index.createAndShareDocumentsDescription": "Etherpad allows you to edit documents collaboratively in real-time, much like a live multi-player editor that runs in your browser.",
"pad.toolbar.bold.title": "Bold (Ctrl+B)",
"pad.toolbar.italic.title": "Italic (Ctrl+I)",
@ -53,6 +61,7 @@
"pad.toolbar.savedRevision.title": "Save Revision",
"pad.toolbar.settings.title": "Settings",
"pad.toolbar.embed.title": "Share and Embed this pad",
"pad.toolbar.home.title": "Back to home",
"pad.toolbar.showusers.title": "Show the users on this pad",
"pad.colorpicker.save": "Save",

View File

@ -14,6 +14,7 @@
"Macofe",
"MartaEgea",
"Mklehr",
"Ovruni",
"Rubenwap",
"Tiberius1701",
"VegaDark",
@ -50,11 +51,13 @@
"admin_settings": "Configuración",
"admin_settings.current": "Configuración actual",
"admin_settings.current_example-devel": "Plantilla de ejemplo de configuración de desarrollo",
"admin_settings.current_example-prod": "Ejemplo de plantilla de configuración de producción",
"admin_settings.current_restart.value": "Reiniciar Etherpad",
"admin_settings.current_save.value": "Guardar configuración",
"admin_settings.page-title": "Configuración. Etherpad",
"index.newPad": "Nuevo pad",
"index.createOpenPad": "o crea/abre un pad con el nombre:",
"index.openPad": "abrir un pad existente con el nombre:",
"pad.toolbar.bold.title": "Negrita (Ctrl-B)",
"pad.toolbar.italic.title": "Cursiva (Ctrl-I)",
"pad.toolbar.underline.title": "Subrayado (Ctrl-U)",
@ -87,6 +90,8 @@
"pad.settings.fontType": "Tipografía:",
"pad.settings.fontType.normal": "Normal",
"pad.settings.language": "Idioma:",
"pad.settings.deletePad": "Eliminar pad",
"pad.delete.confirm": "¿De verdad quieres borrar este pad?",
"pad.settings.about": "Acerca de",
"pad.settings.poweredBy": "Funciona con",
"pad.importExport.import_export": "Importar/Exportar",
@ -123,6 +128,10 @@
"pad.modals.corruptPad.cause": "Esto puede deberse a una mala configuración del servidor o algún otro comportamiento inesperado. Contacta con el administrador del servicio.",
"pad.modals.deleted": "Borrado.",
"pad.modals.deleted.explanation": "Este pad ha sido borrado.",
"pad.modals.rateLimited": "Límite de solicitudes.",
"pad.modals.rateLimited.explanation": "Enviaste demasiados mensajes a este pad por lo que te desconectó.",
"pad.modals.rejected.explanation": "El servidor rechazó un mensaje que envió tu navegador.",
"pad.modals.rejected.cause": "Es posible que el servidor se haya actualizado mientras veías el panel, o que haya un error en Etherpad. Intenta recargar la página.",
"pad.modals.disconnected": "Te has desconectado.",
"pad.modals.disconnected.explanation": "Se perdió la conexión con el servidor",
"pad.modals.disconnected.cause": "El servidor podría no estar disponible. Contacta con el administrador del servicio si esto continúa sucediendo.",
@ -135,6 +144,7 @@
"pad.chat.loadmessages": "Cargar más mensajes",
"pad.chat.stick.title": "Ampliar",
"pad.chat.writeMessage.placeholder": "Enviar un mensaje",
"timeslider.followContents": "Sigue las actualizaciones de contenido del pad",
"timeslider.pageTitle": "{{appTitle}} Línea de tiempo",
"timeslider.toolbar.returnbutton": "Volver al pad",
"timeslider.toolbar.authors": "Autores:",
@ -173,5 +183,6 @@
"pad.impexp.uploadFailed": "El envío falló. Inténtalo de nuevo.",
"pad.impexp.importfailed": "Fallo al importar",
"pad.impexp.copypaste": "Intenta copiar y pegar",
"pad.impexp.exportdisabled": "La exportación al formato {{type}} está desactivada. Contacta con tu administrador del sistema."
"pad.impexp.exportdisabled": "La exportación al formato {{type}} está desactivada. Contacta con tu administrador del sistema.",
"pad.impexp.maxFileSize": "El archivo es demasiado grande. Contacte al administrador del sitio para aumentar el tamaño de archivo permitido para la importación."
}

View File

@ -4,6 +4,7 @@
"BMRG14",
"Beginneruser",
"Dalba",
"Darafsh",
"Ebrahim",
"Ebraminio",
"FarsiNevis",
@ -66,6 +67,7 @@
"pad.toolbar.savedRevision.title": "ذخیره‌سازی نسخه",
"pad.toolbar.settings.title": "تنظیمات",
"pad.toolbar.embed.title": "اشتراک و جاسازی این دفترچه یادداشت",
"pad.toolbar.home.title": "بازگشت به صفحهٔ اصلی",
"pad.toolbar.showusers.title": "نمایش کاربران در این دفترچه یادداشت",
"pad.colorpicker.save": "ذخیره",
"pad.colorpicker.cancel": "لغو",

View File

@ -50,11 +50,13 @@
"admin_settings": "Asetukset",
"admin_settings.current": "Nykyinen kokoonpano",
"admin_settings.current_example-devel": "Esimerkki kehitysasetusten mallista",
"admin_settings.current_example-prod": "Esimerkkipohja tuotantoasetuksille",
"admin_settings.current_restart.value": "Käynnistä Etherpad uudelleen",
"admin_settings.current_save.value": "Tallenna asetukset",
"admin_settings.page-title": "asetukset - Etherpad",
"index.newPad": "Uusi muistio",
"index.createOpenPad": "tai luo tai avaa muistio nimellä:",
"index.openPad": "avaa olemassa oleva muistio nimellä:",
"pad.toolbar.bold.title": "Lihavointi (Ctrl-B)",
"pad.toolbar.italic.title": "Kursivointi (Ctrl-I)",
"pad.toolbar.underline.title": "Alleviivaus (Ctrl-U)",
@ -71,6 +73,7 @@
"pad.toolbar.savedRevision.title": "Tallenna muutos",
"pad.toolbar.settings.title": "Asetukset",
"pad.toolbar.embed.title": "Jaa ja upota muistio",
"pad.toolbar.home.title": "Takaisin kotiin",
"pad.toolbar.showusers.title": "Näytä muistion käyttäjät",
"pad.colorpicker.save": "Tallenna",
"pad.colorpicker.cancel": "Peru",
@ -137,6 +140,7 @@
"pad.chat.loadmessages": "Lataa lisää viestejä",
"pad.chat.stick.title": "Liimaa chatti ruutuun",
"pad.chat.writeMessage.placeholder": "Kirjoita viestisi tähän",
"timeslider.followContents": "Seuraa muistion sisällön päivityksiä",
"timeslider.pageTitle": "{{appTitle}} -aikajana",
"timeslider.toolbar.returnbutton": "Palaa muistioon",
"timeslider.toolbar.authors": "Tekijät:",
@ -175,5 +179,6 @@
"pad.impexp.uploadFailed": "Lähetys epäonnistui. Yritä uudelleen.",
"pad.impexp.importfailed": "Tuonti epäonnistui",
"pad.impexp.copypaste": "Kopioi ja liitä",
"pad.impexp.exportdisabled": "Vienti muotoon \"{{type}}\" ei ole käytössä. Ota yhteys ylläpitäjään saadaksesi lisätietoja."
"pad.impexp.exportdisabled": "Vienti muotoon \"{{type}}\" ei ole käytössä. Ota yhteys ylläpitäjään saadaksesi lisätietoja.",
"pad.impexp.maxFileSize": "Tiedosto on liian suuri. Ota yhteyttä sivustosi ylläpitäjään ja pyydä heitä korottomaan suurinta sallittua tiedostokokoa."
}

View File

@ -5,6 +5,7 @@
"C13m3n7",
"Cquoi",
"Crochet.david",
"Derugon",
"Envlh",
"Framafan",
"Fylip22",
@ -83,6 +84,7 @@
"pad.toolbar.savedRevision.title": "Enregistrer la révision",
"pad.toolbar.settings.title": "Paramètres",
"pad.toolbar.embed.title": "Partager et intégrer ce bloc-notes",
"pad.toolbar.home.title": "Retour à laccueil",
"pad.toolbar.showusers.title": "Afficher les utilisateurs du bloc-notes",
"pad.colorpicker.save": "Enregistrer",
"pad.colorpicker.cancel": "Annuler",

View File

@ -58,6 +58,7 @@
"pad.toolbar.savedRevision.title": "Gardar a revisión",
"pad.toolbar.settings.title": "Axustes",
"pad.toolbar.embed.title": "Compartir e incorporar este documento",
"pad.toolbar.home.title": "Volver ao inicio",
"pad.toolbar.showusers.title": "Mostrar as usuarias deste documento",
"pad.colorpicker.save": "Gardar",
"pad.colorpicker.cancel": "Cancelar",
@ -120,7 +121,7 @@
"pad.modals.disconnected.explanation": "Perdeuse a conexión co servidor",
"pad.modals.disconnected.cause": "O servidor non está dispoñible. Póñase en contacto co administrador do servizo se o problema continúa.",
"pad.share": "Compartir este documento",
"pad.share.readonly": "Só lectura",
"pad.share.readonly": "Lectura só",
"pad.share.link": "Ligazón",
"pad.share.emebdcode": "Incorporar o URL",
"pad.chat": "Chat",

View File

@ -10,7 +10,8 @@
"Notramo",
"Ovari",
"R-Joe",
"Tgr"
"Tgr",
"Urbalazs"
]
},
"admin.page-title": "Admin irányítópult - Etherpad",
@ -67,7 +68,7 @@
"pad.toolbar.embed.title": "Jegyzetfüzet beágyazása és megosztása",
"pad.toolbar.showusers.title": "Jegyzetfüzet felhasználóinak megmutatása",
"pad.colorpicker.save": "Mentés",
"pad.colorpicker.cancel": "Mégsem",
"pad.colorpicker.cancel": "Mégse",
"pad.loading": "Betöltés…",
"pad.noCookie": "Nem található a süti. Engedélyezd a böngésződben a sütik használatát! A munkamenet és a beállítások nem kerülnek mentésre a látogatások között. Ennek oka lehet az, hogy az Etherpad egyes böngészőkben szerepel az iFrame-ben. Ellenőrizze, hogy az Etherpad ugyanabban az altartomány / tartományban van-e, mint a szülő iFrame",
"pad.permissionDenied": "Nincs engedélyed ezen jegyzetfüzet eléréséhez",

View File

@ -56,6 +56,7 @@
"pad.toolbar.savedRevision.title": "Version salveguardate",
"pad.toolbar.settings.title": "Configuration",
"pad.toolbar.embed.title": "Condivider e incorporar iste pad",
"pad.toolbar.home.title": "Retro al initio",
"pad.toolbar.showusers.title": "Monstrar le usatores de iste pad",
"pad.colorpicker.save": "Salveguardar",
"pad.colorpicker.cancel": "Cancellar",

View File

@ -42,6 +42,7 @@
"pad.toolbar.savedRevision.title": "Versione salvata",
"pad.toolbar.settings.title": "Impostazioni",
"pad.toolbar.embed.title": "Condividi ed incorpora questo Pad",
"pad.toolbar.home.title": "Torna alla pagina principale",
"pad.toolbar.showusers.title": "Visualizza gli utenti su questo Pad",
"pad.colorpicker.save": "Salva",
"pad.colorpicker.cancel": "Annulla",

View File

@ -58,6 +58,7 @@
"pad.toolbar.savedRevision.title": "Зачувај преработка",
"pad.toolbar.settings.title": "Поставки",
"pad.toolbar.embed.title": "Споделете и вметнете ја тетраткава",
"pad.toolbar.home.title": "Назад на Почетна",
"pad.toolbar.showusers.title": "Прикажи корисниците на тетраткава",
"pad.colorpicker.save": "Зачувај",
"pad.colorpicker.cancel": "Откажи",

View File

@ -112,7 +112,7 @@
"pad.savedrevs.marked": "Semakan ini telah ditandai sebagai semakan tersimpan",
"pad.savedrevs.timeslider": "Anda boleh melihat semakan yang tersimpan dengan melawat gelangsar masa",
"pad.userlist.entername": "Taipkan nama anda",
"pad.userlist.unnamed": "tanpa nama",
"pad.userlist.unnamed": "tidak bernama",
"pad.editbar.clearcolors": "Padamkan warna pengarang pada seluruh dokumen?",
"pad.impexp.importbutton": "Import Sekarang",
"pad.impexp.importing": "Sedang mengimport...",

View File

@ -12,6 +12,7 @@
]
},
"admin_plugins.description": "विवरण",
"admin_plugins.name": "नाम",
"index.newPad": "नयाँ प्याड",
"index.createOpenPad": "नाम सहितको नयाँ प्याड सिर्जना गर्ने / खोल्ने :",
"pad.toolbar.bold.title": "मोटो (Ctrl-B)",

View File

@ -56,6 +56,7 @@
"pad.toolbar.savedRevision.title": "Argistré la revision",
"pad.toolbar.settings.title": "Paràmeter",
"pad.toolbar.embed.title": "Partagé e antëgré ës feuj",
"pad.toolbar.home.title": "Artorn a l'intrada",
"pad.toolbar.showusers.title": "Smon-e j'utent ansima a 's feuj",
"pad.colorpicker.save": "Argistré",
"pad.colorpicker.cancel": "Anulé",

View File

@ -1,11 +1,12 @@
{
"@metadata": {
"authors": [
"Ahmed-Najib-Biabani-Ibrahimkhel"
"Ahmed-Najib-Biabani-Ibrahimkhel",
"شاه زمان پټان"
]
},
"index.newPad": "نوې ليکچه",
"index.createOpenPad": "يا په همدې نوم يوه نوې ليکچه جوړول/پرانيستل:",
"index.createOpenPad": "يا په همدې نوم يوه نوې ليکچه جوړول/پرانېستل:",
"pad.toolbar.bold.title": "زغرد (Ctrl-B)",
"pad.toolbar.italic.title": "رېوند (Ctrl-I)",
"pad.toolbar.underline.title": "لرکرښن (Ctrl+U)",
@ -16,11 +17,12 @@
"pad.toolbar.redo.title": "بياکړل (Ctrl-Y)",
"pad.toolbar.clearAuthorship.title": "د ليکوالۍ رنگونه سپينول (Ctrl+Shift+C)",
"pad.toolbar.savedRevision.title": "مخکتنه خوندي کول",
"pad.toolbar.settings.title": "امستنې",
"pad.toolbar.settings.title": "اوڼنې",
"pad.toolbar.home.title": "بېرته کور ته",
"pad.colorpicker.save": "خوندي کول",
"pad.colorpicker.cancel": "ناگارل",
"pad.loading": "رابرسېرېږي...",
"pad.settings.padSettings": "د ليکچې امستنې",
"pad.settings.padSettings": "د ليکچې اوڼنې",
"pad.settings.myView": "زما کتنه",
"pad.settings.stickychat": "تل په پردې بانډار کول",
"pad.settings.chatandusers": "کارنان او بانډار ښکاره کول",

View File

@ -71,6 +71,7 @@
"pad.toolbar.savedRevision.title": "Сохранить версию",
"pad.toolbar.settings.title": "Настройки",
"pad.toolbar.embed.title": "Поделиться и встроить этот документ",
"pad.toolbar.home.title": "Вернуться в начало",
"pad.toolbar.showusers.title": "Показать пользователей в документе",
"pad.colorpicker.save": "Сохранить",
"pad.colorpicker.cancel": "Отмена",

View File

@ -1,8 +1,7 @@
{
"@metadata": {
"authors": [
"Conquistador",
"Vlad5250"
"Winston Sung"
]
},
"admin_plugins.available_not-found": "Nijedan plugin nije pronađen.",

View File

@ -84,6 +84,8 @@
"pad.settings.fontType": "Тип шрифту:",
"pad.settings.fontType.normal": "Звичайний",
"pad.settings.language": "Мова:",
"pad.settings.deletePad": "Вилучити документ",
"pad.delete.confirm": "Ви дійсно хочете вилучити цей документ?",
"pad.settings.about": "Про програму",
"pad.settings.poweredBy": "Працює на",
"pad.importExport.import_export": "Імпорт/Експорт",

View File

@ -66,6 +66,7 @@
"pad.toolbar.savedRevision.title": "儲存修訂版",
"pad.toolbar.settings.title": "設定",
"pad.toolbar.embed.title": "分享和嵌入此記事本",
"pad.toolbar.home.title": "返回首頁",
"pad.toolbar.showusers.title": "顯示此記事本的使用者",
"pad.colorpicker.save": "儲存",
"pad.colorpicker.cancel": "取消",

View File

@ -1,13 +1,15 @@
'use strict';
// @ts-nocheck
const DB = require('./DB');
const Store = require('@etherpad/express-session').Store;
import expressSession from 'express-session'
const log4js = require('log4js');
const util = require('util');
const logger = log4js.getLogger('SessionStore');
class SessionStore extends Store {
class SessionStore extends expressSession.Store {
/**
* @param {?number} [refresh] - How often (in milliseconds) `touch()` will update a session's
* database record with the cookie's latest expiration time. If the difference between the
@ -19,7 +21,7 @@ class SessionStore extends Store {
* Etherpad is restarted. Use `null` to prevent `touch()` from ever updating the record.
* Ignored if the cookie does not expire.
*/
constructor(refresh = null) {
constructor(refresh: number | null = null) {
super();
this._refresh = refresh;
// Maps session ID to an object with the following properties:

View File

@ -4,12 +4,10 @@ import {Socket} from "node:net";
import type {MapArrayType} from "../types/MapType";
import _ from 'underscore';
// @ts-ignore
import cookieParser from 'cookie-parser';
import events from 'events';
import express from 'express';
// @ts-ignore
import expressSession from '@etherpad/express-session';
import expressSession, {Store} from 'express-session';
import fs from 'fs';
const hooks = require('../../static/js/pluginfw/hooks');
import log4js from 'log4js';
@ -24,7 +22,7 @@ import SecretRotator from '../security/SecretRotator';
let secretRotator: SecretRotator|null = null;
const logger = log4js.getLogger('http');
let serverName:string;
let sessionStore: { shutdown: () => void; } | null;
let sessionStore: Store | null;
const sockets:Set<Socket> = new Set();
const socketsEvents = new events.EventEmitter();
const startTime = stats.settableGauge('httpStartTime');
@ -59,6 +57,7 @@ const closeServer = async () => {
startTime.setValue(0);
logger.info('HTTP server closed');
}
// @ts-ignore
if (sessionStore) sessionStore.shutdown();
sessionStore = null;
if (secretRotator) secretRotator.stop();
@ -198,10 +197,9 @@ exports.restartServer = async () => {
sessionStore = new SessionStore(settings.cookie.sessionRefreshInterval);
exports.sessionMiddleware = expressSession({
propagateTouch: true,
rolling: true,
secret,
store: sessionStore,
store: sessionStore ?? undefined,
resave: false,
saveUninitialized: false,
// Set the cookie name to a javascript identifier compatible string. Makes code handling it
@ -234,7 +232,7 @@ exports.restartServer = async () => {
// Give plugins an opportunity to install handlers/middleware before the express-session
// middleware. This allows plugins to avoid creating an express-session record in the database
// when it is not needed (e.g., public static content).
await hooks.aCallAll('expressPreSession', {app});
await hooks.aCallAll('expressPreSession', {app, settings});
app.use(exports.sessionMiddleware);
app.use(webaccess.checkAccess);

View File

@ -39,6 +39,11 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
})
}
socket.on('getStats', ()=>{
console.log("Getting stats for admin plugins");
socket.emit('results:stats', require('../../stats').toJSON());
})
socket.on('getInstalled', async (query: string) => {
// send currently installed plugins
const installed =
@ -53,6 +58,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
socket.emit('results:installed', {installed});
});
socket.on('checkUpdates', async () => {
// Check plugins for updates
try {

View File

@ -1,16 +1,39 @@
'use strict';
import express from "express";
const log4js = require('log4js');
const clientLogger = log4js.getLogger('client');
const {Formidable} = require('formidable');
const apiHandler = require('../../handler/APIHandler');
const util = require('util');
function objectAsString(obj: any): string {
let output = '';
for (const property in obj) {
if(obj.hasOwnProperty(property) && typeof obj[property] !== 'function') {
let value = obj[property];
if(typeof value === 'object' && !Array.isArray(value) && value !== null) {
value = '{' + objectAsString(value) + '}';
}
output += property + ': ' + value +'; ';
}
}
return output;
}
exports.expressPreSession = async (hookName:string, {app}:any) => {
app.use(express.json());
// The Etherpad client side sends information about how a disconnect happened
app.post('/ep/pad/connection-diagnostic-info', async (req:any, res:any) => {
const [fields, files] = await (new Formidable({})).parse(req);
clientLogger.info(`DIAGNOSTIC-INFO: ${fields.diagnosticInfo}`);
if (!req.body ||!req.body.diagnosticInfo || typeof req.body.diagnosticInfo !== 'object') {
clientLogger.warn('DIAGNOSTIC-INFO: No diagnostic info provided');
res.status(400).end('No diagnostic info provided');
return;
}
clientLogger.info(`DIAGNOSTIC-INFO: ${objectAsString(req.body.diagnosticInfo)}`);
res.end('OK');
});

View File

@ -20,7 +20,7 @@ exports.socketio = (hookName: string, {io}: any) => {
}
exports.expressPreSession = async (hookName:string, {app}:ArgsExpressType) => {
exports.expressPreSession = async (hookName:string, {app, settings}:ArgsExpressType) => {
// This endpoint is intended to conform to:
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
app.get('/health', (req:any, res:any) => {
@ -31,9 +31,12 @@ exports.expressPreSession = async (hookName:string, {app}:ArgsExpressType) => {
});
});
app.get('/stats', (req:any, res:any) => {
res.json(require('../../stats').toJSON());
});
if (settings.enableMetrics) {
app.get('/stats', (req:any, res:any) => {
res.json(require('../../stats').toJSON());
});
}
app.get('/javascript', (req:any, res:any) => {
res.send(eejs.require('ep_etherpad-lite/templates/javascript.html', {req}));

View File

@ -1,7 +1,9 @@
import {Express} from "express";
import {MapArrayType} from "./MapType";
export type ArgsExpressType = {
app:Express,
io: any,
server:any
settings: MapArrayType<any>
}

View File

@ -205,6 +205,12 @@ exports.padOptions = {
lang: null,
};
/**
* Wether to enable the /stats endpoint. The functionality in the admin menu is untouched for this.
*/
exports.enableMetrics = true
/**
* Whether certain shortcut keys are enabled for a user in the pad
*/
@ -245,7 +251,7 @@ exports.toolbar = {
],
right: [
['importexport', 'timeslider', 'savedrevision'],
['settings', 'embed'],
['settings', 'embed', 'home'],
['showusers'],
],
timeslider: [

View File

@ -240,6 +240,7 @@ module.exports = {
settings: defaultButtonAttributes('settings'),
embed: defaultButtonAttributes('embed'),
showusers: defaultButtonAttributes('showusers'),
home: defaultButtonAttributes('home'),
timeslider_export: {
command: 'import_export',

View File

@ -30,23 +30,23 @@
}
],
"dependencies": {
"@etherpad/express-session": "^1.18.4",
"async": "^3.2.6",
"axios": "^1.8.4",
"axios": "^1.10.0",
"cookie-parser": "^1.4.7",
"cross-env": "^7.0.3",
"cross-spawn": "^7.0.6",
"ejs": "^3.1.10",
"esbuild": "^0.25.5",
"esbuild": "^0.25.8",
"express": "4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^8.0.0",
"express-session": "^1.18.2",
"fast-deep-equal": "^3.1.3",
"find-root": "1.1.0",
"formidable": "^3.5.4",
"http-errors": "^2.0.0",
"jose": "^5.10.0",
"js-cookie": "^3.0.5",
"jsdom": "^26.0.0",
"jsdom": "^26.1.0",
"jsonminify": "0.4.2",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
@ -57,8 +57,8 @@
"lru-cache": "^11.1.0",
"measured-core": "^2.0.0",
"mime-types": "^3.0.1",
"oidc-provider": "^9.1.3",
"openapi-backend": "^5.12.0",
"oidc-provider": "^9.3.0",
"openapi-backend": "^5.13.0",
"proxy-addr": "^2.0.7",
"rate-limiter-flexible": "^7.1.1",
"rehype": "^13.0.2",
@ -69,7 +69,7 @@
"semver": "^7.7.2",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"superagent": "10.2.1",
"superagent": "10.2.2",
"swagger-ui-express": "^5.0.1",
"tinycon": "0.6.8",
"tsx": "4.20.3",
@ -83,19 +83,21 @@
"etherpad-lite": "node/server.ts"
},
"devDependencies": {
"@playwright/test": "^1.53.0",
"@playwright/test": "^1.54.1",
"@types/async": "^3.2.24",
"@types/cookie-parser": "^1.4.9",
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.2",
"@types/formidable": "^3.4.5",
"@types/http-errors": "^2.0.5",
"@types/jquery": "^3.5.32",
"@types/js-cookie": "^3.0.6",
"@types/jsdom": "^21.1.7",
"@types/jsonwebtoken": "^9.0.9",
"@types/jsonwebtoken": "^9.0.10",
"@types/mime-types": "^3.0.1",
"@types/mocha": "^10.0.9",
"@types/node": "^24.0.1",
"@types/oidc-provider": "^9.1.0",
"@types/node": "^24.0.14",
"@types/oidc-provider": "^9.1.1",
"@types/semver": "^7.7.0",
"@types/sinon": "^17.0.3",
"@types/supertest": "^6.0.2",
@ -103,19 +105,19 @@
"@types/underscore": "^1.13.0",
"@types/whatwg-mimetype": "^3.0.2",
"chokidar": "^4.0.3",
"eslint": "^9.28.0",
"eslint": "^9.31.0",
"eslint-config-etherpad": "^4.0.4",
"etherpad-cli-client": "^3.0.2",
"mocha": "^11.6.0",
"etherpad-cli-client": "^3.0.4",
"mocha": "^11.7.1",
"mocha-froth": "^0.2.10",
"nodeify": "^1.0.1",
"openapi-schema-validation": "^0.4.2",
"set-cookie-parser": "^2.7.1",
"sinon": "^21.0.0",
"split-grid": "^1.0.11",
"supertest": "^7.1.1",
"supertest": "^7.1.3",
"typescript": "^5.8.2",
"vitest": "^3.2.3"
"vitest": "^3.2.4"
},
"engines": {
"node": ">=18.18.2",
@ -142,6 +144,6 @@
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts",
"test:vitest": "vitest"
},
"version": "2.3.2",
"version": "2.4.0",
"license": "Apache-2.0"
}

View File

@ -101,7 +101,7 @@
.buttonicon-pencil-alt:before { content: '\e808'; } /* '' */
.buttonicon-file-code:before { content: '\e809'; } /* '' */
.buttonicon-mail:before { content: '\e80a'; } /* '' */
.buttonicon-home:before { content: '\e80b'; } /* '' */
.buttonicon-home:before { content: '\e80b'; font-size: 20px } /* '' */
.buttonicon-trash:before { content: '\e80e'; } /* '' */
.buttonicon-times:before { content: '\e826'; } /* '' */
.buttonicon-pause:before { content: '\e829'; } /* '' */

View File

@ -2526,7 +2526,15 @@ function Ace2Inner(editorInfo, cssManagers) {
const handleKeyEvent = (evt) => {
if (!isEditable) return;
const {type, charCode, keyCode, which, altKey, shiftKey} = evt;
const {type, charCode, keyCode, which, shiftKey} = evt;
// If DOM3 support exists, ensure that the left ALT key was pressed. This
// allows keyboard layouts with special meaning for right-alt-char to
// continue working on Firefox / macOS.
let altKey = evt.altKey;
if (evt.originalEvent.location !== undefined) {
altKey = altKey && evt.originalEvent.location === evt.originalEvent.DOM_KEY_LOCATION_LEFT;
}
// Don't take action based on modifier keys going up and down.
// Modifier keys do not generate "keypress" events.

View File

@ -7,8 +7,6 @@ let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
if (language) regexpLang = language[1];
html10n.mt.bind('indexed', () => {
console.log('Navigator language', navigator.language)
console.log('Localizing things', [regexpLang, navigator.language, 'en'])
html10n.localize([regexpLang, navigator.language, 'en']);
});

View File

@ -725,18 +725,18 @@ const pad = {
}
},
asyncSendDiagnosticInfo: () => {
window.setTimeout(() => {
$.ajax(
{
type: 'post',
url: '../ep/pad/connection-diagnostic-info',
data: {
diagnosticInfo: JSON.stringify(pad.diagnosticInfo),
},
success: () => {},
error: () => {},
});
}, 0);
const currentUrl = window.location.href;
fetch('../ep/pad/connection-diagnostic-info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
diagnosticInfo: pad.diagnosticInfo,
}),
}).catch((error) => {
console.error('Error sending diagnostic info:', error);
})
},
forceReconnect: () => {
$('form#reconnectform input.padId').val(pad.getPadId());

View File

@ -364,6 +364,9 @@ exports.padeditbar = new class {
this.registerDropdownCommand('connectivity');
this.registerDropdownCommand('import_export');
this.registerDropdownCommand('embed');
this.registerCommand('home', ()=>{
window.location.href = window.location.href + "/../.."
})
this.registerCommand('settings', () => {
this.toggleDropDown('settings');

View File

@ -489,6 +489,15 @@ const paduserlist = (() => {
online++;
}
}
const recentPadsList = JSON.parse(localStorage.getItem('recentPads'));
const pathSegments = window.location.pathname.split('/');
const padName = pathSegments[pathSegments.length - 1];
const existingPad = recentPadsList.find((pad) => pad.name === padName);
if (existingPad) {
existingPad.members = online;
}
localStorage.setItem('recentPads', JSON.stringify(recentPadsList));
$('#online_count').text(online);

View File

@ -16,7 +16,6 @@ class Scroll {
this.outerWin = outerWin;
this.doc = this.outerWin.contentDocument!;
this.rootDocument = document;
console.log(this.rootDocument)
}
scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary(rep: RepModel, isScrollableEvent: boolean, innerHeight: number) {

View File

@ -472,7 +472,6 @@ export class Html10n {
}
localize(langs: (string|undefined)[]|string) {
console.log('Available langs ', langs)
if ('string' === typeof langs) {
langs = [langs];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,88 +1,258 @@
#button,
body,
form {
border: none
:root {
--etherpad-color: #64d29b;
--etherpad-color-dark: #4a5d5c;
--etherpad-border: oklch(92.8% 0.006 264.531);
--muted-text: oklch(44.6% 0.03 256.802);
--muted-border: oklch(87.2% 0.01 258.338);
--muted-background: hsl(240 4.8% 95.9%);
--ep-color: rgb(22 163 74);
--warm-green-olive: #7c9a3e;
--warm-green-moss: #8fae4a;
--warm-green-lime: #b7c96c;
--warm-green-avocado: #6e8b3d;
--warm-green-spring: #a3c85a;
}
body {
background: url(images/fond.jpg) center center no-repeat fixed #fff;
font-family: Quicksand, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-size: 16px;
line-height: 1.42857143;
color: #333;
border-top: 0;
background: oklch(98.5% 0.002 247.839);
display: flex;
align-items: center;
justify-content: center;
background-size: cover;
flex-direction: column;
}
#wrapper {
border-top: none;
margin-top: 0;
padding: 0;
background: 0 0;
box-shadow: none
h1 {
margin: auto 0 0;
font-size: 26px;
}
input {
color: #4a5d5c;
}
#inner {
background: transparent;
padding-top: 0;
width: 350px;
max-width: 350px;
text-align: center;
color:#FFF;
}
#label {
text-shadow: none;
color: #FFF;
font-weight: normal;
text-align: center;
}
#button {
margin: 0 auto;
text-align: center;
width: 100%;
text-shadow: none;
font-size: 23px;
line-height: 1.8;
color: #64d29b;
background: #586a69;
border-radius: 3px;
box-shadow: none;
height: 53px;
border: none;
.mission-statement, .pad-datalist {
display: block;
}
button[type=submit] {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
left: 305px;
color: #64d29b;
background: #586a69;
border: none;
top: 0;
opacity: 1;
transition: .2s background;
.mission-statement h2 {
font-weight: 700;
font-size: 2.25rem;
text-align: center;
margin: 0;
padding-top: 4rem;
}
#button:hover,
button[type=submit]:hover {
cursor: pointer;
background: #4a5d5c;
color: #64d29b;
.mission-statement p {
color: var(--muted-text);
font-size: 20px;
text-align: center;
max-width: 40%;
margin: auto;
}
#wrapper {
border-top: 0;
margin-top: 0;
padding: 0;
background: unset;
box-shadow: none;
}
#inner {
display: flex;
flex-direction: column;
position: relative;
margin-top: 20px;
margin-bottom: 20px;
max-width: 80%;
}
#label {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: rgb(55 65 81);
margin-bottom: 0.5rem;
margin-top: 0;
}
#go2Name {
order: 1;
}
#padname, #go2Name, #go2Name [type="submit"], #button, #button:hover {
all: unset;
}
#padname {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 1rem;
margin-bottom: 0.5rem;
outline: none;
transition: border 0.2s;
}
#padname {
height: 38px;
max-width: 350px;
padding: 0 12px;
box-sizing: border-box;
width: 100%;
color: var(--muted-text);
border: 1px solid var(--muted-border);
border-radius: 5px;
}
#button, #button:hover, #go2Name [type="submit"] {
order: 2;
margin-top: 0.5rem;
line-height: 1.25rem;
background: white;
border: 1px solid var(--muted-border);
text-align: center;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
font-size: 14px;
font-weight: 700;
border-radius: 5px;
cursor: pointer;
}
#go2Name [type="submit"]:hover {
background-color: oklch(52.7% 0.154 150.069)
}
#button, #button:hover {
order: 2;
}
#button:hover {
background-color: var(--muted-background);
}
#go2Name input {
width: 100%;
}
#go2Name [type="submit"] {
display: block;
background-color: var(--ep-color);
color: white;
width: 100%;
}
body nav {
display: flex;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--etherpad-border);
padding: 1rem 1.5rem;
}
.logo-box svg {
width: 1.25rem;
height: 1.25rem;
color: #fff;
}
.logo-box {
width: 2rem;
height: 2rem;
background: #16a34a;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
}
#wrapper, .pad-datalist {
width: 100%;
max-width: 28rem;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
box-shadow: 0 1px 2px 0 #0001;
margin: 2rem auto auto;
}
.pad-datalist {
max-width: 56rem;
margin-bottom: 1rem;
}
.break-column {
flex-basis: 100%;
width: 0;
}
ul {
list-style-type: none;
}
.recent-pad {
padding: 0.75rem 1.5rem;
display: flex;
position: relative;
}
flex-direction: column;
}
.body {
flex-grow: 1;
background: linear-gradient(
to bottom right,
#d1fae5, /* emerald-100 */
#f0fdfa, /* teal-50 */
#dbeafe, /* blue-100 */
#c7d2fe /* indigo-200 */
);
}
.recent-pad:hover a {
color: var(--ep-color);
}
.recent-pad-arrow {
position: absolute;
right: 1rem;
}
.recent-pad a {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 800;
}
a, a:visited, a:hover, a:active {
color: inherit;
}
.pad-datalist h2 {
border-bottom: 1px solid var(--muted-border);
padding: 1rem 1.5rem;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: #e5e7eb;
}
.card-content {
padding: 1.5rem;
}
@media (max-width: 640px) {
#inner {
max-width: 100%;
padding: 0 1rem;
}
.mission-statement p {
max-width: 100%;
}
.pad-datalist {
max-width: 90%;
}
.mission-statement h2 {
font-size: 1.5rem;
}
}

View File

@ -1,7 +1,137 @@
'use strict';
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
window.customStart();
} else {
window.addEventListener('DOMContentLoaded', window.customStart, {once: true});
}
}
});
window.customStart = () => {
document.getElementById('recent-pads').replaceChildren()
// define your javascript here
// jquery is available - except index.js
// you can load extra scripts with $.getScript http://api.jquery.com/jQuery.getScript/
const divHoldingPlaceHolderLabel = document
.querySelector('[data-l10n-id="index.placeholderPadEnter"]');
const observer = new MutationObserver(() => {
document.querySelector('#go2Name input')
.setAttribute('placeholder', divHoldingPlaceHolderLabel.textContent);
});
observer
.observe(divHoldingPlaceHolderLabel, {childList: true, subtree: true, characterData: true});
const recentPadList = document.getElementById('recent-pads');
const parentStyle = recentPadList.parentElement.style;
const recentPadListHeading = document.querySelector('[data-l10n-id="index.recentPads"]');
const recentPadsFromLocalStorage = localStorage.getItem('recentPads');
let recentPadListData = [];
if (recentPadsFromLocalStorage != null) {
recentPadListData = JSON.parse(recentPadsFromLocalStorage);
}
// Remove duplicates based on pad name and sort by timestamp
recentPadListData = recentPadListData.filter(
(pad, index, self) =>
index === self.findIndex((p) => p.name === pad.name)
).sort((a, b) => new Date(a.timestamp) > new Date(b.timestamp) ? -1 : 1);
if (recentPadListData.length === 0) {
recentPadListHeading.setAttribute('data-l10n-id', 'index.recentPadsEmpty');
parentStyle.display = 'flex';
parentStyle.justifyContent = 'center';
parentStyle.alignItems = 'center';
parentStyle.maxHeight = '100%';
recentPadList.remove();
} else {
/**
* @typedef {Object} Pad
* @property {string} name
*/
/**
* @param {Pad} pad
*/
const arrowIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right w-4 h-4 text-gray-400"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg>';
const clockIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock w-3 h-3"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>';
const personalIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users w-3 h-3"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>';
recentPadListData.forEach((pad) => {
const li = document.createElement('li');
li.style.cursor = 'pointer';
li.className = 'recent-pad';
const padPath = `${window.location.href}p/${pad.name}`;
const link = document.createElement('a');
link.style.textDecoration = 'none';
link.href = padPath;
link.innerText = pad.name;
li.appendChild(link);
const arrowIconElement = document.createElement('span');
arrowIconElement.className = 'recent-pad-arrow';
arrowIconElement.innerHTML = arrowIcon;
li.appendChild(arrowIconElement);
const nextRow = document.createElement('div');
nextRow.style.display = 'flex';
nextRow.style.gap = '10px';
nextRow.style.marginTop = '10px';
const clockIconElement = document.createElement('span');
clockIconElement.className = 'recent-pad-clock';
clockIconElement.innerHTML = clockIcon;
nextRow.appendChild(clockIconElement);
const time = new Date(pad.timestamp);
const userLocale = navigator.language || 'en-US';
const formattedTime = time.toLocaleDateString(userLocale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
const timeElement = document.createElement('span');
timeElement.className = 'recent-pad-time';
timeElement.innerText = formattedTime;
nextRow.appendChild(timeElement);
const personalIconElement = document.createElement('span');
personalIconElement.className = 'recent-pad-personal';
personalIconElement.innerHTML = personalIcon;
personalIconElement.style.marginLeft = '5px';
const members = document.createElement('span');
members.className = 'recent-pad-members';
members.innerText = pad.members;
nextRow.appendChild(personalIconElement);
nextRow.appendChild(members);
li.appendChild(nextRow);
li.addEventListener('click', () => {
window.location.href = padPath;
});
// https://v0.dev/chat/etherpad-design-clone-qZnwOrVRXxH
recentPadList.appendChild(li);
});
}
};

View File

@ -1,7 +1,35 @@
'use strict';
const MAX_PADS_IN_HISTORY = 3;
window.customStart = () => {
$('#pad_title').show();
$('.buttonicon').on('mousedown', function () { $(this).parent().addClass('pressed'); });
$('.buttonicon').on('mouseup', function () { $(this).parent().removeClass('pressed'); });
const pathSegments = window.location.pathname.split('/');
const padName = pathSegments[pathSegments.length - 1];
const recentPads = localStorage.getItem('recentPads');
if (recentPads == null) {
localStorage.setItem('recentPads', JSON.stringify([]));
}
const recentPadsList = JSON.parse(localStorage.getItem('recentPads'));
if (!recentPadsList.some((pad) => pad.name === padName)) {
if (recentPadsList.length >= MAX_PADS_IN_HISTORY) {
recentPadsList.shift(); // Remove the oldest pad if we have more than 10
}
recentPadsList.push({
name: padName,
timestamp: new Date().toISOString(), // Store the timestamp for sorting
members: 1,
});
localStorage.setItem('recentPads', JSON.stringify(recentPadsList));
} else {
// Update the timestamp if the pad already exists
const existingPad = recentPadsList.find((pad) => pad.name === padName);
if (existingPad) {
existingPad.timestamp = new Date().toISOString();
}
localStorage.setItem('recentPads', JSON.stringify(recentPadsList));
}
};

View File

@ -109,6 +109,10 @@
width: 45px;
height: 38px;
}
nav, .mission-statement, .pad-datalist {
display: none;
}
@media only screen and (min-device-width: 320px) and (max-device-width: 800px) {
body {
background: #bbb;
@ -131,24 +135,45 @@
<link href="static/skins/<%=encodeURI(settings.skinName)%>/index.css?v=<%=settings.randomVersionString%>" rel="stylesheet">
<% e.end_block(); %>
<nav>
<div class="logo-box">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-text w-5 h-5 text-white"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"></path><path d="M14 2v4a2 2 0 0 0 2 2h4"></path><path d="M10 9H8"></path><path d="M16 13H8"></path><path d="M16 17H8"></path></svg>
</div>
<h1>Etherpad</h1>
</nav>
<div class="body">
<div class="mission-statement">
<h2 data-l10n-id="index.createAndShareDocuments"></h2>
<p data-l10n-id="index.createAndShareDocumentsDescription"></p>
</div>
<div id="wrapper">
<% e.begin_block("indexWrapper"); %>
<div id="inner">
<% if (!settings.requireSession) { %>
<% if (settings.editOnly) { %>
<label id="label" for="padname" data-l10n-id="index.openPad"></label>
<button data-l10n-id="index.openPad"></button>
<% } else {%>
<button id="button" data-l10n-id="index.newPad"></button>
<label id="label" for="padname" data-l10n-id="index.createOpenPad"></label>
<button id="button" data-l10n-id="index.generateNewPad"></button>
<% } %>
<form action="#" id="go2Name">
<input type="text" id="padname" maxlength="50" autofocus x-webkit-speech>
<button type="submit">OK</button>
<label id="label" for="padname" data-l10n-id="index.labelPad"></label>
<input type="text" id="padname" maxlength="50" autofocus placeholder="Enter pad name...">
<button type="submit" data-l10n-id="index.createOpenPad"></button>
</form>
<% } %>
</div>
<% e.end_block(); %>
</div>
<div style="display: none" data-l10n-id="index.placeholderPadEnter"></div>
<div class="pad-datalist">
<h2 data-l10n-id="index.recentPads"></h2>
<ul id="recent-pads">
</ul>
</div>
</div>
<script src="<%=entrypoint%>"></script>
<% e.begin_block("indexCustomScripts"); %>

View File

@ -164,7 +164,7 @@
</p>
<% e.end_block(); %>
</div>
<button data-l10n-id="pad.settings.delete" id="delete-pad">Delete pad</button>
<button data-l10n-id="pad.settings.deletePad" id="delete-pad">Delete pad</button>
<h2 data-l10n-id="pad.settings.about">About</h2>
<span data-l10n-id="pad.settings.poweredBy">Powered by</span>
<a href="https://etherpad.org" target="_blank" referrerpolicy="no-referrer" rel="noopener">Etherpad</a>

View File

@ -13,7 +13,6 @@ test.describe('Plugins page', ()=> {
await page.waitForSelector('.search-field');
const pluginTable = page.locator('table tbody').nth(1);
await expect(pluginTable).not.toBeEmpty()
await expect(pluginTable.locator('tr')).toHaveCount(190)
})
test('Searches for a plugin', async ({page}) => {
@ -33,8 +32,6 @@ test.describe('Plugins page', ()=> {
await expect(pluginTable).not.toBeEmpty({
timeout: 15000
})
const plugins = await pluginTable.locator('tr').count()
await expect(pluginTable.locator('tr')).toHaveCount(190)
// Now everything is loaded, lets install a plugin

View File

@ -0,0 +1,17 @@
import {expect, test} from "@playwright/test";
import {clearPadContent, getPadBody, goToNewPad} from "../helper/padHelper";
test.beforeEach(async ({ page })=>{
// create a new pad before each test run
await goToNewPad(page);
})
test('should go to home on pad', async ({page}) => {
const homeButton = page.locator('.buttonicon.buttonicon-home')
const attribute = await homeButton.getAttribute('data-l10n-id')
expect(attribute).toBe('pad.toolbar.home.title');
await homeButton.click();
const url = page.url();
expect(url).not.toContain('/p/');
})

View File

@ -38,7 +38,7 @@ test.describe('indentation button', function () {
await page.locator('.buttonicon-indent').click()
// type a bit, make a line break and type again
await padBody.locator('div').first().focus()
await padBody.focus()
await page.keyboard.type('line 1')
await page.keyboard.press('Enter');
await page.keyboard.type('line 2')

View File

@ -12,6 +12,6 @@
"devDependencies": {
"ep_etherpad-lite": "workspace:../src",
"typescript": "^5.8.2",
"vite": "^6.3.5"
"vite": "^7.0.4"
}
}