mirror of
https://github.com/daniel-j/fimfic2epub.git
synced 2024-05-14 01:13:50 +12:00
Make binary a webpack target, add static/standalone build target
This commit is contained in:
parent
12c935d698
commit
12177f2ec2
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,4 +8,5 @@ extension.zip
|
|||
fimfic2epub.safariextension/
|
||||
dist/
|
||||
build/
|
||||
bin/
|
||||
*.epub
|
||||
|
|
|
@ -1,94 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const args = require('commander')
|
||||
.command('fimfic2epub <story> [filename]')
|
||||
.description(require('../package.json').description)
|
||||
.version(require('../package.json').version)
|
||||
.option('-d, --dir <path>', 'Directory to store ebook in. Is prepended to filename')
|
||||
.option('-t, --title <value>', 'Set the title of the story')
|
||||
.option('-a, --author <value>', 'Set the author of the story')
|
||||
.option('-c, --no-comments-link', 'Don\'t add link to online comments')
|
||||
.option('-H, --no-headings', 'Don\'t add headings to chapters')
|
||||
.option('-r, --no-reading-ease', 'Don\'t calculate Flesch reading ease')
|
||||
.option('-e, --no-external', 'Don\'t embed external resources, such as images (breaks EPUB spec)')
|
||||
.option('-n, --no-notes', 'Don\'t include author notes')
|
||||
.option('-i, --notes-index', 'Create an index with all author notes at the end of the ebook')
|
||||
.option('-p, --paragraphs <style>', 'Select a paragraph style <spaced|indented|indentedall|both>', 'spaced')
|
||||
.option('-j, --join-subjects', 'Join dc:subjects to a single value')
|
||||
.option('-C, --cover <url>', 'Set cover image url')
|
||||
.parse(process.argv)
|
||||
|
||||
if (args.args.length < 1) {
|
||||
console.error('Error: No story id/url provided')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const outputStdout = args.args[1] === '-' || args.args[1] === '/dev/stdout'
|
||||
|
||||
if (outputStdout) {
|
||||
console.log = console.error
|
||||
console.log('Outputting to stdout')
|
||||
}
|
||||
|
||||
// use a mock DOM so we can run mithril on the server
|
||||
require('mithril/test-utils/browserMock')(global)
|
||||
|
||||
const FimFic2Epub = require('../dist/fimfic2epub')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const STORY_ID = args.args[0]
|
||||
|
||||
const ffc = new FimFic2Epub(STORY_ID, {
|
||||
addCommentsLink: !!args.commentsLink,
|
||||
includeAuthorNotes: !!args.notes,
|
||||
useAuthorNotesIndex: !!args.notesIndex,
|
||||
addChapterHeadings: !!args.headings,
|
||||
includeExternal: !!args.external,
|
||||
paragraphStyle: args.paragraphs,
|
||||
joinSubjects: !!args.joinSubjects,
|
||||
calculateReadingEase: !!args.readingEase,
|
||||
readingEaseWakeupInterval: 800
|
||||
})
|
||||
ffc.coverUrl = args.cover
|
||||
|
||||
ffc.fetchMetadata()
|
||||
.then(() => {
|
||||
if (args.title) {
|
||||
ffc.setTitle(args.title)
|
||||
}
|
||||
if (args.author) {
|
||||
ffc.setAuthorName(args.author)
|
||||
}
|
||||
})
|
||||
.then(ffc.fetchAll.bind(ffc))
|
||||
.then(ffc.build.bind(ffc))
|
||||
.then(() => {
|
||||
let filename = (args.args[1] || '').replace('%id%', ffc.storyInfo.id) || ffc.filename
|
||||
let stream
|
||||
|
||||
if (args.dir) {
|
||||
filename = path.join(args.dir, filename)
|
||||
}
|
||||
|
||||
if (outputStdout) {
|
||||
stream = process.stdout
|
||||
} else {
|
||||
stream = fs.createWriteStream(filename)
|
||||
}
|
||||
ffc.streamFile(null)
|
||||
.pipe(stream)
|
||||
.on('finish', () => {
|
||||
if (!outputStdout) {
|
||||
console.log('Saved story as ' + filename)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && err.stack) {
|
||||
console.error(err.stack)
|
||||
} else {
|
||||
console.error('Error: ' + (err || 'Unknown error'))
|
||||
}
|
||||
process.exit(1)
|
||||
})
|
||||
// fimfic2epub 1.7.15
|
||||
!function(e){var t={};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.m=e,o.c=t,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},o.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o.w={},o(o.s=6)}([function(e,t){e.exports=require("../package.json")},function(e,t){e.exports=require("path")},function(e,t){e.exports=require("fs")},function(e,t){e.exports=require("../dist/fimfic2epub")},function(e,t){e.exports=require("mithril/test-utils/browserMock")},function(e,t){e.exports=require("commander")},function(e,t,o){"use strict";const n=o(5).command("fimfic2epub <story> [filename]").description(o(0).description).version(o(0).version).option("-d, --dir <path>","Directory to store ebook in. Is prepended to filename").option("-t, --title <value>","Set the title of the story").option("-a, --author <value>","Set the author of the story").option("-c, --no-comments-link","Don't add link to online comments").option("-H, --no-headings","Don't add headings to chapters").option("-r, --no-reading-ease","Don't calculate Flesch reading ease").option("-e, --no-external","Don't embed external resources, such as images (breaks EPUB spec)").option("-n, --no-notes","Don't include author notes").option("-i, --notes-index","Create an index with all author notes at the end of the ebook").option("-p, --paragraphs <style>","Select a paragraph style <spaced|indented|indentedall|both>","spaced").option("-j, --join-subjects","Join dc:subjects to a single value").option("-C, --cover <url>","Set cover image url").parse(process.argv);n.args.length<1&&(console.error("Error: No story id/url provided"),process.exit(1));const r="-"===n.args[1]||"/dev/stdout"===n.args[1];r&&(console.log=console.error,console.log("Outputting to stdout")),o(4)(global);const s=o(3),i=o(2),a=o(1),c=new s(n.args[0],{addCommentsLink:!!n.commentsLink,includeAuthorNotes:!!n.notes,useAuthorNotesIndex:!!n.notesIndex,addChapterHeadings:!!n.headings,includeExternal:!!n.external,paragraphStyle:n.paragraphs,joinSubjects:!!n.joinSubjects,calculateReadingEase:!!n.readingEase,readingEaseWakeupInterval:800});c.coverUrl=n.cover,c.fetchMetadata().then(()=>{n.title&&c.setTitle(n.title),n.author&&c.setAuthorName(n.author)}).then(c.fetchAll.bind(c)).then(c.build.bind(c)).then(()=>{let e,t=(n.args[1]||"").replace("%id%",c.storyInfo.id)||c.filename;n.dir&&(t=a.join(n.dir,t)),e=r?process.stdout:i.createWriteStream(t),c.streamFile(null).pipe(e).on("finish",()=>{r||console.log("Saved story as "+t)})}).catch(e=>{e&&e.stack?console.error(e.stack):console.error("Error: "+(e||"Unknown error")),process.exit(1)})}]);
|
|
@ -10,6 +10,8 @@ import filter from 'gulp-filter'
|
|||
import merge from 'merge-stream'
|
||||
import change from 'gulp-change'
|
||||
import rename from 'gulp-rename'
|
||||
import banner from 'gulp-banner'
|
||||
import chmod from 'gulp-chmod'
|
||||
|
||||
import jsonedit from 'gulp-json-editor'
|
||||
import zip from 'gulp-zip'
|
||||
|
@ -57,7 +59,11 @@ function webpackTask (callback) {
|
|||
hash: false,
|
||||
version: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
timings: false,
|
||||
modules: false,
|
||||
chunkModules: false,
|
||||
cached: false,
|
||||
maxModules: 0
|
||||
}))
|
||||
sequence('pack', callback)
|
||||
})
|
||||
|
@ -80,6 +86,7 @@ let lintPipe = lazypipe()
|
|||
|
||||
// Cleanup task
|
||||
gulp.task('clean', () => del([
|
||||
'bin/',
|
||||
'build/',
|
||||
'extension/build/',
|
||||
'dist/',
|
||||
|
@ -97,6 +104,13 @@ gulp.task('version', (done) => {
|
|||
|
||||
// Main tasks
|
||||
gulp.task('webpack', ['version', 'fontawesome'], webpackTask)
|
||||
gulp.task('binaries', ['version'], () => {
|
||||
return gulp.src(['build/fimfic2epub.js', 'build/fimfic2epub-static.js'])
|
||||
.pipe(rename({ extname: '' }))
|
||||
.pipe(banner('#!/usr/bin/env node\n// fimfic2epub ' + packageVersion + '\n'))
|
||||
.pipe(chmod(0o777))
|
||||
.pipe(gulp.dest('bin/'))
|
||||
})
|
||||
gulp.task('watch:webpack', () => {
|
||||
return watch(['src/**/*.js', 'src/**/*.styl', './package.json'], watchOpts, () => {
|
||||
return sequence('webpack')
|
||||
|
@ -104,10 +118,10 @@ gulp.task('watch:webpack', () => {
|
|||
})
|
||||
|
||||
gulp.task('lint', () => {
|
||||
return gulp.src(['gulpfile.babel.js', 'webpack.config.babel.js', 'src/**/*.js', 'bin/fimfic2epub']).pipe(lintPipe())
|
||||
return gulp.src(['gulpfile.babel.js', 'webpack.config.babel.js', 'src/**/*.js']).pipe(lintPipe())
|
||||
})
|
||||
gulp.task('watch:lint', () => {
|
||||
return watch(['src/**/*.js', 'gulpfile.babel.js', 'webpack.config.babel.js', 'bin/fimfic2epub'], watchOpts, (file) => {
|
||||
return watch(['src/**/*.js', 'gulpfile.babel.js', 'webpack.config.babel.js'], watchOpts, (file) => {
|
||||
return gulp.src(file.path).pipe(lintPipe())
|
||||
})
|
||||
})
|
||||
|
@ -135,7 +149,7 @@ gulp.task('fontawesome', () => {
|
|||
.pipe(gulp.dest('build/'))
|
||||
return merge(copy, codes)
|
||||
})
|
||||
gulp.task('pack', (done) => {
|
||||
gulp.task('pack', ['binaries'], (done) => {
|
||||
sequence(['pack:firefox', 'pack:chrome'], done)
|
||||
})
|
||||
gulp.task('watch:pack', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fimfic2epub",
|
||||
"version": "1.7.14",
|
||||
"version": "1.7.15",
|
||||
"description": "Tool to generate improved EPUB ebooks from Fimfiction stories",
|
||||
"author": "djazz",
|
||||
"license": "MIT",
|
||||
|
@ -18,7 +18,7 @@
|
|||
"main": "dist/fimfic2epub.js",
|
||||
"files": [
|
||||
"dist/",
|
||||
"bin/",
|
||||
"bin/fimfic2epub",
|
||||
"LICENSE"
|
||||
],
|
||||
"dependencies": {
|
||||
|
@ -52,12 +52,15 @@
|
|||
"babel-preset-es2015": "^6.14.0",
|
||||
"babel-preset-node6": "^11.0.0",
|
||||
"babel-register": "^6.26.0",
|
||||
"binary-loader": "0.0.1",
|
||||
"del": "^3.0.0",
|
||||
"es6-event-emitter": "^1.10.2",
|
||||
"exports-loader": "^0.7.0",
|
||||
"file-saver": "^1.3.2",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-banner": "^0.1.3",
|
||||
"gulp-change": "^1.0.0",
|
||||
"gulp-chmod": "^2.0.0",
|
||||
"gulp-filter": "^5.0.1",
|
||||
"gulp-json-editor": "^2.2.1",
|
||||
"gulp-rename": "^1.2.2",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* global chrome */
|
||||
|
||||
import path from 'path'
|
||||
import JSZip from 'jszip'
|
||||
import escapeStringRegexp from 'escape-string-regexp'
|
||||
import zeroFill from 'zero-fill'
|
||||
|
@ -613,12 +612,7 @@ class FimFic2Epub extends Emitter {
|
|||
if (!isNode) {
|
||||
fontPath = chrome.extension.getURL('build/fonts/fontawesome-webfont.ttf')
|
||||
} else {
|
||||
// TODO: Fix better font detection
|
||||
const fs = require('fs')
|
||||
fontPath = path.join(__dirname, '../node_modules/', 'font-awesome/fonts/fontawesome-webfont.ttf')
|
||||
if (!fs.existsSync(fontPath)) {
|
||||
fontPath = path.join(__dirname, '../../', 'font-awesome/fonts/fontawesome-webfont.ttf')
|
||||
}
|
||||
fontPath = require('font-awesome/fonts/fontawesome-webfont.ttf') // resolve the path, see webpack config
|
||||
}
|
||||
this.iconsFont = await subsetFont(fontPath, glyphs, {local: isNode})
|
||||
}
|
||||
|
|
93
src/cli.js
Executable file
93
src/cli.js
Executable file
|
@ -0,0 +1,93 @@
|
|||
|
||||
const args = require('commander')
|
||||
.command('fimfic2epub <story> [filename]')
|
||||
.description(require('../package.json').description)
|
||||
.version(require('../package.json').version)
|
||||
.option('-d, --dir <path>', 'Directory to store ebook in. Is prepended to filename')
|
||||
.option('-t, --title <value>', 'Set the title of the story')
|
||||
.option('-a, --author <value>', 'Set the author of the story')
|
||||
.option('-c, --no-comments-link', 'Don\'t add link to online comments')
|
||||
.option('-H, --no-headings', 'Don\'t add headings to chapters')
|
||||
.option('-r, --no-reading-ease', 'Don\'t calculate Flesch reading ease')
|
||||
.option('-e, --no-external', 'Don\'t embed external resources, such as images (breaks EPUB spec)')
|
||||
.option('-n, --no-notes', 'Don\'t include author notes')
|
||||
.option('-i, --notes-index', 'Create an index with all author notes at the end of the ebook')
|
||||
.option('-p, --paragraphs <style>', 'Select a paragraph style <spaced|indented|indentedall|both>', 'spaced')
|
||||
.option('-j, --join-subjects', 'Join dc:subjects to a single value')
|
||||
.option('-C, --cover <url>', 'Set cover image url')
|
||||
.parse(process.argv)
|
||||
|
||||
if (args.args.length < 1) {
|
||||
console.error('Error: No story id/url provided')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const outputStdout = args.args[1] === '-' || args.args[1] === '/dev/stdout'
|
||||
|
||||
if (outputStdout) {
|
||||
console.log = console.error
|
||||
console.log('Outputting to stdout')
|
||||
}
|
||||
|
||||
// use a mock DOM so we can run mithril on the server
|
||||
require('mithril/test-utils/browserMock')(global)
|
||||
|
||||
const FimFic2Epub = require('./FimFic2Epub')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const STORY_ID = args.args[0]
|
||||
|
||||
const ffc = new FimFic2Epub(STORY_ID, {
|
||||
addCommentsLink: !!args.commentsLink,
|
||||
includeAuthorNotes: !!args.notes,
|
||||
useAuthorNotesIndex: !!args.notesIndex,
|
||||
addChapterHeadings: !!args.headings,
|
||||
includeExternal: !!args.external,
|
||||
paragraphStyle: args.paragraphs,
|
||||
joinSubjects: !!args.joinSubjects,
|
||||
calculateReadingEase: !!args.readingEase,
|
||||
readingEaseWakeupInterval: 800
|
||||
})
|
||||
ffc.coverUrl = args.cover
|
||||
|
||||
ffc.fetchMetadata()
|
||||
.then(() => {
|
||||
if (args.title) {
|
||||
ffc.setTitle(args.title)
|
||||
}
|
||||
if (args.author) {
|
||||
ffc.setAuthorName(args.author)
|
||||
}
|
||||
})
|
||||
.then(ffc.fetchAll.bind(ffc))
|
||||
.then(ffc.build.bind(ffc))
|
||||
.then(() => {
|
||||
let filename = (args.args[1] || '').replace('%id%', ffc.storyInfo.id) || ffc.filename
|
||||
let stream
|
||||
|
||||
if (args.dir) {
|
||||
filename = path.join(args.dir, filename)
|
||||
}
|
||||
|
||||
if (outputStdout) {
|
||||
stream = process.stdout
|
||||
} else {
|
||||
stream = fs.createWriteStream(filename)
|
||||
}
|
||||
ffc.streamFile(null)
|
||||
.pipe(stream)
|
||||
.on('finish', () => {
|
||||
if (!outputStdout) {
|
||||
console.log('Saved story as ' + filename)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && err.stack) {
|
||||
console.error(err.stack)
|
||||
} else {
|
||||
console.error('Error: ' + (err || 'Unknown error'))
|
||||
}
|
||||
process.exit(1)
|
||||
})
|
|
@ -3,18 +3,25 @@ import isNode from 'detect-node'
|
|||
import { Font } from 'fonteditor-core'
|
||||
import fs from 'fs'
|
||||
import fetch from './fetch'
|
||||
import fileType from 'file-type'
|
||||
|
||||
async function subsetFont (fontPath, glyphs, options = {}) {
|
||||
let data
|
||||
if (!isNode || !options.local) {
|
||||
data = await fetch(fontPath, 'arraybuffer')
|
||||
} else {
|
||||
data = await new Promise((resolve, reject) => {
|
||||
fs.readFile(fontPath, (err, data) => {
|
||||
if (err) reject(err)
|
||||
else resolve(data)
|
||||
let fontdata = Buffer.from(fontPath, 'binary')
|
||||
let type = fileType(fontdata)
|
||||
if (type && type.mime === 'font/ttf') {
|
||||
data = fontdata
|
||||
} else {
|
||||
data = await new Promise((resolve, reject) => {
|
||||
fs.readFile(fontPath, (err, data) => {
|
||||
if (err) reject(err)
|
||||
else resolve(data)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
return Font.create(data, {
|
||||
type: 'ttf',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
import webpack from 'webpack'
|
||||
import path from 'path'
|
||||
import nodeExternals from 'webpack-node-externals'
|
||||
|
||||
|
@ -49,6 +50,7 @@ const bundleExtensionConfig = {
|
|||
|
||||
plugins: [
|
||||
// new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)()
|
||||
new webpack.NormalModuleReplacementPlugin(/font-awesome\/fonts\/fontawesome-webfont\.ttf/, './false')
|
||||
],
|
||||
performance: {
|
||||
hints: false
|
||||
|
@ -106,7 +108,9 @@ const bundleNpmModuleConfig = {
|
|||
__dirname: false
|
||||
},
|
||||
|
||||
externals: [nodeExternals({whitelist: ['es6-event-emitter', /^babel-runtime/]})],
|
||||
externals: [nodeExternals({whitelist: ['es6-event-emitter', /^babel-runtime/]}), {
|
||||
'font-awesome/fonts/fontawesome-webfont.ttf': 'require.resolve(\'font-awesome/fonts/fontawesome-webfont.ttf\')'
|
||||
}],
|
||||
|
||||
plugins: [],
|
||||
performance: {
|
||||
|
@ -120,4 +124,131 @@ const bundleNpmModuleConfig = {
|
|||
mode: inProduction ? 'production' : 'development'
|
||||
}
|
||||
|
||||
export default [bundleExtensionConfig, bundleNpmModuleConfig]
|
||||
const bundleNpmBinaryConfig = {
|
||||
entry: './src/cli',
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, '/'),
|
||||
filename: './build/fimfic2epub.js'
|
||||
},
|
||||
|
||||
target: 'node',
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
query: {
|
||||
sourceMaps: !inProduction,
|
||||
presets: [['env', {
|
||||
targets: {
|
||||
node: '8.0.0'
|
||||
}
|
||||
}]]
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.styl$/,
|
||||
use: ['raw-loader', 'stylus-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: ['.js', '.json', '.styl'],
|
||||
modules: [
|
||||
path.resolve('./src'),
|
||||
'node_modules'
|
||||
]
|
||||
},
|
||||
|
||||
node: {
|
||||
__dirname: false
|
||||
},
|
||||
|
||||
externals: [nodeExternals(), {
|
||||
'./FimFic2Epub': 'require(\'../dist/fimfic2epub\')',
|
||||
'../package.json': 'require(\'../package.json\')',
|
||||
'font-awesome/fonts/fontawesome-webfont.ttf': 'require.resolve(\'font-awesome/fonts/fontawesome-webfont.ttf\')'
|
||||
}],
|
||||
|
||||
plugins: [],
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
optimization: {
|
||||
concatenateModules: inProduction,
|
||||
minimize: inProduction
|
||||
},
|
||||
devtool: false,
|
||||
mode: inProduction ? 'production' : 'development'
|
||||
}
|
||||
|
||||
const bundleStaticNpmModuleConfig = {
|
||||
entry: './src/cli',
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, '/'),
|
||||
filename: './build/fimfic2epub-static.js'
|
||||
},
|
||||
|
||||
target: 'node',
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
query: {
|
||||
sourceMaps: !inProduction,
|
||||
presets: [['env', {
|
||||
targets: {
|
||||
node: '9.0.0'
|
||||
}
|
||||
}]]
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.styl$/,
|
||||
use: ['raw-loader', 'stylus-loader']
|
||||
},
|
||||
{
|
||||
test: /\.ttf$/,
|
||||
use: 'binary-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: ['.js', '.json', '.styl'],
|
||||
modules: [
|
||||
path.resolve('./bin'),
|
||||
'node_modules'
|
||||
]
|
||||
},
|
||||
|
||||
node: {
|
||||
__dirname: false
|
||||
},
|
||||
|
||||
plugins: [],
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
optimization: {
|
||||
concatenateModules: inProduction,
|
||||
minimize: inProduction
|
||||
},
|
||||
devtool: false,
|
||||
mode: inProduction ? 'production' : 'development'
|
||||
}
|
||||
|
||||
export default [
|
||||
bundleExtensionConfig,
|
||||
bundleNpmModuleConfig,
|
||||
bundleNpmBinaryConfig,
|
||||
bundleStaticNpmModuleConfig
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue