hyde_admin 0.0.13 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/.rspec +3 -0
- data/CHANGELOG.md +31 -0
- data/CLAUDE.md +56 -0
- data/Gemfile +8 -0
- data/bin/admin_views/admin_layout.html.erb +76 -51
- data/bin/admin_views/configuration.erb +4 -4
- data/bin/admin_views/dashboard.erb +37 -1
- data/bin/admin_views/editor_html.erb +21 -18
- data/bin/admin_views/editor_js.erb +112 -111
- data/bin/admin_views/files/edit.erb +4 -4
- data/bin/admin_views/files/listing.erb +14 -14
- data/bin/admin_views/posts/edit.erb +159 -62
- data/bin/admin_views/posts/listing.erb +10 -10
- data/bin/admin_views/search.erb +42 -0
- data/bin/admin_views/upload_image_form.erb +5 -5
- data/bin/hyde_admin.ru +165 -11
- data/bin/hyde_assets/hyde_admin.css +58 -0
- data/bin/hyde_assets/hyde_admin.js +121 -18
- data/bin/i18n/en.yml +20 -1
- data/bin/i18n/fr.yml +21 -2
- data/hyde_admin.gemspec +6 -8
- data/lib/hyde_admin/version.rb +1 -1
- data/spec/files/create_file_spec.rb +92 -0
- data/spec/mid_helpers_spec.rb +105 -0
- data/spec/posts/create_draft_spec.rb +77 -0
- data/spec/posts/create_page_spec.rb +75 -0
- data/spec/posts/create_post_spec.rb +100 -0
- data/spec/search/global_search_spec.rb +92 -0
- data/spec/spec_helper.rb +25 -0
- metadata +26 -34
- data/.idea/.gitignore +0 -8
- data/.idea/hyde_admin.iml +0 -25
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/vcs.xml +0 -6
data/bin/hyde_admin.ru
CHANGED
|
@@ -5,7 +5,8 @@ require 'yaml'
|
|
|
5
5
|
require 'fileutils'
|
|
6
6
|
require 'i18n'
|
|
7
7
|
require 'date'
|
|
8
|
-
require '
|
|
8
|
+
require 'cgi'
|
|
9
|
+
require 'shellwords'
|
|
9
10
|
require 'image_processing/mini_magick'
|
|
10
11
|
require_relative '../lib/hyde_admin/version'
|
|
11
12
|
|
|
@@ -37,14 +38,14 @@ class Mid < Roda
|
|
|
37
38
|
if !File.exist?(yml_in_current_dir)
|
|
38
39
|
FileUtils.cp(yml_in_gem, yml_in_current_dir)
|
|
39
40
|
end
|
|
40
|
-
@hyde_parameters ||= YAML.
|
|
41
|
+
@hyde_parameters ||= YAML.safe_load(File.read(yml_in_current_dir), permitted_classes: [Date, Time])
|
|
41
42
|
|
|
42
43
|
super(param)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def self.transliterate_title_for_url(title)
|
|
46
47
|
I18n.config.available_locales = :en
|
|
47
|
-
I18n.transliterate(title).downcase.gsub(/[^a-zA-
|
|
48
|
+
I18n.transliterate(title).downcase.gsub(/[^a-zA-Z0-9 ]/,'').strip.gsub(/\s+/,'-').gsub(/-+/,'-').chomp('-')
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def self.urlize(date_str, title, with_date = true)
|
|
@@ -62,7 +63,7 @@ class Mid < Roda
|
|
|
62
63
|
|
|
63
64
|
def self.extract_header(str)
|
|
64
65
|
headers = Mid.extract_header_str(str).to_s.split("\n")
|
|
65
|
-
headers = headers.select{ |header| !header.empty? }.map{ |header| header.scan(/([a-zA-Z0-
|
|
66
|
+
headers = headers.select{ |header| !header.empty? }.map{ |header| header.scan(/([a-zA-Z0-9_-]+): (.*)/).flatten }.select{ |header| !header.empty? }
|
|
66
67
|
hsh_headers = {}
|
|
67
68
|
if !headers.flatten.empty?
|
|
68
69
|
#$stderr.puts "==============="
|
|
@@ -80,6 +81,10 @@ class Mid < Roda
|
|
|
80
81
|
File.expand_path(File.dirname(__FILE__))
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
def self.safe_path?(path)
|
|
85
|
+
File.expand_path(path).start_with?(Dir.pwd)
|
|
86
|
+
end
|
|
87
|
+
|
|
83
88
|
def self.extract_tags(str)
|
|
84
89
|
str.to_s.scan(/^\[?(.*?)\]?$/).flatten.first.split(',')
|
|
85
90
|
end
|
|
@@ -112,6 +117,7 @@ class Mid < Roda
|
|
|
112
117
|
|
|
113
118
|
route do |r|
|
|
114
119
|
@page = r.params['page']
|
|
120
|
+
@notice = r.params['notice']
|
|
115
121
|
|
|
116
122
|
if @hyde_parameters['hyde_admin_auth'].to_s == 'true'
|
|
117
123
|
http_auth {|u, p| [u, p] == [@hyde_parameters['hyde_admin_user'], @hyde_parameters['hyde_admin_password']] }
|
|
@@ -134,13 +140,46 @@ class Mid < Roda
|
|
|
134
140
|
# Rebuild static files
|
|
135
141
|
r.on "rebuild" do
|
|
136
142
|
$stderr.puts Dir.pwd
|
|
137
|
-
$stderr.puts `cd #{Dir.pwd} && jekyll b`
|
|
138
|
-
r.redirect "/dashboard"
|
|
143
|
+
$stderr.puts `cd #{Shellwords.escape(Dir.pwd)} && jekyll b`
|
|
144
|
+
r.redirect "/dashboard?notice=#{CGI.escape(t.rebuild + ' OK')}"
|
|
139
145
|
end
|
|
140
146
|
|
|
141
147
|
r.on "deploy" do
|
|
142
|
-
`#{@hyde_parameters['rsync_fullpath']} -avzr #{Dir.pwd}/_site/ #{@hyde_parameters['deploy_dest_user']}@#{@hyde_parameters['deploy_dest_address']}:#{@hyde_parameters['deploy_dest_path']}`
|
|
143
|
-
r.redirect "/dashboard"
|
|
148
|
+
`#{Shellwords.escape(@hyde_parameters['rsync_fullpath'])} -avzr #{Shellwords.escape("#{Dir.pwd}/_site/")} #{Shellwords.escape(@hyde_parameters['deploy_dest_user'])}@#{Shellwords.escape(@hyde_parameters['deploy_dest_address'])}:#{Shellwords.escape(@hyde_parameters['deploy_dest_path'])}`
|
|
149
|
+
r.redirect "/dashboard?notice=#{CGI.escape(t.deploy + ' OK')}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
r.on "deploy_sftp" do
|
|
153
|
+
site_dir = "#{Dir.pwd}/_site"
|
|
154
|
+
sftp_user = @hyde_parameters['sftp_user']
|
|
155
|
+
sftp_host = @hyde_parameters['sftp_host']
|
|
156
|
+
sftp_port = @hyde_parameters['sftp_port'].to_s.empty? ? '22' : @hyde_parameters['sftp_port']
|
|
157
|
+
sftp_path = @hyde_parameters['sftp_dest_path']
|
|
158
|
+
sftp_password = @hyde_parameters['sftp_password']
|
|
159
|
+
|
|
160
|
+
batch_commands = []
|
|
161
|
+
Dir.glob("#{site_dir}/**/*").each do |local_path|
|
|
162
|
+
relative = local_path.sub("#{site_dir}/", '')
|
|
163
|
+
remote = "#{sftp_path}/#{relative}"
|
|
164
|
+
if File.directory?(local_path)
|
|
165
|
+
batch_commands << "-mkdir #{Shellwords.escape(remote)}"
|
|
166
|
+
else
|
|
167
|
+
batch_commands << "put #{Shellwords.escape(local_path)} #{Shellwords.escape(remote)}"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
batch_file = File.join(Dir.pwd, '.sftp_batch')
|
|
172
|
+
File.write(batch_file, batch_commands.join("\n"))
|
|
173
|
+
|
|
174
|
+
sftp_cmd = "sftp -P #{Shellwords.escape(sftp_port)} -b #{Shellwords.escape(batch_file)}"
|
|
175
|
+
if !sftp_password.to_s.empty?
|
|
176
|
+
sftp_cmd = "sshpass -p #{Shellwords.escape(sftp_password)} #{sftp_cmd}"
|
|
177
|
+
end
|
|
178
|
+
sftp_cmd += " #{Shellwords.escape(sftp_user)}@#{Shellwords.escape(sftp_host)}"
|
|
179
|
+
|
|
180
|
+
`#{sftp_cmd}`
|
|
181
|
+
File.delete(batch_file) if File.exist?(batch_file)
|
|
182
|
+
r.redirect "/dashboard?notice=#{CGI.escape(t.deploy_sftp + ' OK')}"
|
|
144
183
|
end
|
|
145
184
|
|
|
146
185
|
r.post "configuration" do
|
|
@@ -151,7 +190,7 @@ class Mid < Roda
|
|
|
151
190
|
File.open(File.join(Dir.pwd, YML_FILE_NAME),"w+") do |f|
|
|
152
191
|
f.write(@hyde_parameters.to_yaml)
|
|
153
192
|
end
|
|
154
|
-
r.redirect "/configuration"
|
|
193
|
+
r.redirect "/configuration?notice=#{CGI.escape(t.configuration + ' OK')}"
|
|
155
194
|
end
|
|
156
195
|
|
|
157
196
|
r.get "configuration" do
|
|
@@ -162,6 +201,79 @@ class Mid < Roda
|
|
|
162
201
|
view("dashboard")
|
|
163
202
|
end
|
|
164
203
|
|
|
204
|
+
r.get "search" do
|
|
205
|
+
@query = (r.params['q'] || '').strip
|
|
206
|
+
@results = []
|
|
207
|
+
|
|
208
|
+
if @query.length >= 2
|
|
209
|
+
normalized_query = @query.unicode_normalize(:nfkd).gsub(/\p{Mn}/, '').downcase
|
|
210
|
+
|
|
211
|
+
%w[_posts _pages _drafts].each do |dir|
|
|
212
|
+
type_name = dir.sub('_', '')
|
|
213
|
+
dir_path = File.join(Dir.pwd, dir)
|
|
214
|
+
next unless File.directory?(dir_path)
|
|
215
|
+
|
|
216
|
+
Dir.glob(File.join(dir_path, '**', '*')).select { |f| File.file?(f) }.each do |f|
|
|
217
|
+
content = File.read(f, encoding: 'utf-8') rescue next
|
|
218
|
+
headers = Mid.extract_header(content)
|
|
219
|
+
body = Mid.remove_header(content)
|
|
220
|
+
relative = f.gsub(File.join(Dir.pwd, ''), '')
|
|
221
|
+
searchable = [
|
|
222
|
+
File.basename(f),
|
|
223
|
+
headers['title'].to_s,
|
|
224
|
+
headers['tags'].to_s,
|
|
225
|
+
body.to_s[0, 500]
|
|
226
|
+
].join(' ').unicode_normalize(:nfkd).gsub(/\p{Mn}/, '').downcase
|
|
227
|
+
|
|
228
|
+
if searchable.include?(normalized_query)
|
|
229
|
+
excerpt = nil
|
|
230
|
+
body_normalized = body.to_s.unicode_normalize(:nfkd).gsub(/\p{Mn}/, '').downcase
|
|
231
|
+
pos = body_normalized.index(normalized_query)
|
|
232
|
+
if pos
|
|
233
|
+
start = [pos - 60, 0].max
|
|
234
|
+
excerpt = body.to_s[start, 150].gsub(/\s+/, ' ').strip
|
|
235
|
+
excerpt = "...#{excerpt}..." if start > 0
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
@results << {
|
|
239
|
+
type: type_name,
|
|
240
|
+
path: f,
|
|
241
|
+
relative: relative,
|
|
242
|
+
title: headers['title'].to_s.empty? ? File.basename(f) : headers['title'],
|
|
243
|
+
tags: headers['tags'].to_s,
|
|
244
|
+
excerpt: excerpt,
|
|
245
|
+
edit_url: "/#{type_name}?file=#{CGI.escape(f)}"
|
|
246
|
+
}
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Search in files (root directory, excluding _ dirs and _site)
|
|
252
|
+
Dir.glob(File.join(Dir.pwd, '**', '*')).select { |f|
|
|
253
|
+
File.file?(f) &&
|
|
254
|
+
!f.include?('/_') &&
|
|
255
|
+
!f.include?('/_site') &&
|
|
256
|
+
!f.start_with?(File.join(Dir.pwd, '_'))
|
|
257
|
+
}.each do |f|
|
|
258
|
+
relative = f.gsub(File.join(Dir.pwd, ''), '')
|
|
259
|
+
searchable = File.basename(f).unicode_normalize(:nfkd).gsub(/\p{Mn}/, '').downcase
|
|
260
|
+
if searchable.include?(normalized_query)
|
|
261
|
+
@results << {
|
|
262
|
+
type: 'files',
|
|
263
|
+
path: f,
|
|
264
|
+
relative: relative,
|
|
265
|
+
title: File.basename(f),
|
|
266
|
+
tags: '',
|
|
267
|
+
excerpt: nil,
|
|
268
|
+
edit_url: "/files/edit?dir_path=#{CGI.escape(File.dirname(f))}&file=#{CGI.escape(f)}"
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
view("search")
|
|
275
|
+
end
|
|
276
|
+
|
|
165
277
|
r.on "upload_image_form" do
|
|
166
278
|
render("upload_image_form")
|
|
167
279
|
end
|
|
@@ -171,8 +283,12 @@ class Mid < Roda
|
|
|
171
283
|
@filenames = []
|
|
172
284
|
files.each do |file|
|
|
173
285
|
filename = file[:filename]
|
|
286
|
+
ext = File.extname(filename)
|
|
287
|
+
base = File.basename(filename, ext)
|
|
288
|
+
counter = 1
|
|
174
289
|
while File.exist?(File.join(@hyde_parameters['images_path'], filename))
|
|
175
|
-
filename = "#{
|
|
290
|
+
filename = "#{base}_#{counter}#{ext}"
|
|
291
|
+
counter += 1
|
|
176
292
|
end
|
|
177
293
|
@filenames << filename
|
|
178
294
|
File.open(File.join(@hyde_parameters['images_path'], filename), 'wb') do |f|
|
|
@@ -187,6 +303,7 @@ class Mid < Roda
|
|
|
187
303
|
|
|
188
304
|
r.on "files" do
|
|
189
305
|
@dir_path = r.params['dir_path'] || Dir.pwd
|
|
306
|
+
response.status = 403 and next unless Mid.safe_path?(@dir_path)
|
|
190
307
|
|
|
191
308
|
# List files
|
|
192
309
|
r.get "index" do
|
|
@@ -225,6 +342,7 @@ class Mid < Roda
|
|
|
225
342
|
# Edit text files
|
|
226
343
|
r.get "edit" do
|
|
227
344
|
@file = r.params['file']
|
|
345
|
+
response.status = 403 and next unless Mid.safe_path?(@file)
|
|
228
346
|
@content = File.read(@file)
|
|
229
347
|
@header = Mid.extract_header_str(@content)
|
|
230
348
|
@content = Mid.remove_header(@content)
|
|
@@ -236,6 +354,7 @@ class Mid < Roda
|
|
|
236
354
|
# Update file
|
|
237
355
|
r.post "update" do
|
|
238
356
|
@file = r.params['file']
|
|
357
|
+
response.status = 403 and next unless Mid.safe_path?(@file)
|
|
239
358
|
@content = r.params['content']
|
|
240
359
|
@header = r.params['header']
|
|
241
360
|
File.open(@file,"w+") do |f|
|
|
@@ -253,7 +372,12 @@ class Mid < Roda
|
|
|
253
372
|
# Delete
|
|
254
373
|
r.post "delete" do
|
|
255
374
|
file = r.params['file']
|
|
256
|
-
|
|
375
|
+
response.status = 403 and next unless Mid.safe_path?(file)
|
|
376
|
+
if File.directory?(file)
|
|
377
|
+
FileUtils.rm_rf(file) if Dir[File.join(file, '*')].empty?
|
|
378
|
+
else
|
|
379
|
+
File.unlink(file)
|
|
380
|
+
end
|
|
257
381
|
r.redirect "/files/index?dir_path=#{@dir_path}"
|
|
258
382
|
end
|
|
259
383
|
end
|
|
@@ -276,6 +400,34 @@ class Mid < Roda
|
|
|
276
400
|
date = Time.now.strftime(FORMAT_DATE_INPUT_FILENAME)
|
|
277
401
|
response.write(date)
|
|
278
402
|
end
|
|
403
|
+
r.post "preview" do
|
|
404
|
+
content = r.params['content']
|
|
405
|
+
title = r.params['title']
|
|
406
|
+
date = r.params['date']
|
|
407
|
+
layout = r.params['layout']
|
|
408
|
+
tags = r.params['tags']
|
|
409
|
+
format = r.params['format'] || 'md'
|
|
410
|
+
|
|
411
|
+
headers = ["---"]
|
|
412
|
+
headers << "title: #{title}" unless title.to_s.empty?
|
|
413
|
+
headers << "date: #{date}" unless date.to_s.empty?
|
|
414
|
+
headers << "layout: #{layout}" unless layout.to_s.empty?
|
|
415
|
+
headers << "tags: #{tags}" unless tags.to_s.empty?
|
|
416
|
+
headers << "---"
|
|
417
|
+
headers << ""
|
|
418
|
+
|
|
419
|
+
preview_file = File.join(Dir.pwd, "hyde-preview-tmp.#{format}")
|
|
420
|
+
File.open(preview_file, "w+") do |f|
|
|
421
|
+
f.write(headers.join("\n"))
|
|
422
|
+
f.write(content)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
`cd #{Shellwords.escape(Dir.pwd)} && jekyll b 2>&1`
|
|
426
|
+
|
|
427
|
+
File.delete(preview_file) if File.exist?(preview_file)
|
|
428
|
+
|
|
429
|
+
response.write("/hyde-preview-tmp.html?t=#{Time.now.to_i}")
|
|
430
|
+
end
|
|
279
431
|
r.post "images" do
|
|
280
432
|
nb_elements_per_page = 9
|
|
281
433
|
|
|
@@ -325,6 +477,7 @@ class Mid < Roda
|
|
|
325
477
|
# save the truc post
|
|
326
478
|
r.post "delete" do
|
|
327
479
|
@file = r.params['file']
|
|
480
|
+
response.status = 403 and next unless Mid.safe_path?(@file)
|
|
328
481
|
File.unlink(@file)
|
|
329
482
|
r.redirect "/#{@type_file}/index?dir_path=#{File.dirname(@file)}"
|
|
330
483
|
end
|
|
@@ -334,6 +487,7 @@ class Mid < Roda
|
|
|
334
487
|
# edit the truc post
|
|
335
488
|
r.get do
|
|
336
489
|
@file = r.params['file']
|
|
490
|
+
response.status = 403 and next unless Mid.safe_path?(@file)
|
|
337
491
|
|
|
338
492
|
content_file = File.read(@file)
|
|
339
493
|
@headers = Mid.extract_header(content_file)
|
|
@@ -41,11 +41,69 @@
|
|
|
41
41
|
max-width: 90vw;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
#autosave-indicator {
|
|
45
|
+
display: inline-block;
|
|
46
|
+
margin-left: 12px;
|
|
47
|
+
font-size: 0.85em;
|
|
48
|
+
color: #6c757d;
|
|
49
|
+
opacity: 0;
|
|
50
|
+
transition: opacity 0.3s ease;
|
|
51
|
+
}
|
|
52
|
+
#autosave-indicator.visible {
|
|
53
|
+
opacity: 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
.CodeMirror{
|
|
45
57
|
border: 1px solid #ced4da;
|
|
46
58
|
border-radius: 4px;
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
/* Preview split-screen */
|
|
62
|
+
#preview-wrapper.preview-active {
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-direction: column;
|
|
65
|
+
height: calc(100vh - 56px);
|
|
66
|
+
}
|
|
67
|
+
#preview-wrapper.preview-active #editor-panel {
|
|
68
|
+
flex: 1;
|
|
69
|
+
overflow-y: auto;
|
|
70
|
+
min-height: 0;
|
|
71
|
+
padding-right: 8px;
|
|
72
|
+
}
|
|
73
|
+
#preview-wrapper.preview-active #preview-panel {
|
|
74
|
+
flex: 1;
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
min-height: 0;
|
|
78
|
+
border-top: 2px solid #dee2e6;
|
|
79
|
+
}
|
|
80
|
+
#preview-panel .preview-panel-header {
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
}
|
|
83
|
+
#preview-iframe {
|
|
84
|
+
flex: 1;
|
|
85
|
+
width: 100%;
|
|
86
|
+
height: 100%;
|
|
87
|
+
border: none;
|
|
88
|
+
min-height: 0;
|
|
89
|
+
}
|
|
90
|
+
main.preview-active-main {
|
|
91
|
+
padding: 8px !important;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
}
|
|
94
|
+
@media (min-width: 1900px) {
|
|
95
|
+
#preview-wrapper.preview-active {
|
|
96
|
+
flex-direction: row;
|
|
97
|
+
}
|
|
98
|
+
#preview-wrapper.preview-active #editor-panel {
|
|
99
|
+
width: 50%;
|
|
100
|
+
}
|
|
101
|
+
#preview-wrapper.preview-active #preview-panel {
|
|
102
|
+
border-top: none;
|
|
103
|
+
border-left: 2px solid #dee2e6;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
49
107
|
/* hyde admin style */
|
|
50
108
|
.sidebarMenu-logo{
|
|
51
109
|
width: 144px;
|
|
@@ -1,24 +1,127 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function(){
|
|
2
|
+
document.addEventListener('click', function(e){
|
|
3
|
+
var btn = e.target.closest('#btn-deploy, #btn-rebuild');
|
|
4
|
+
if(!btn) return;
|
|
5
|
+
e.preventDefault();
|
|
6
|
+
var path = '';
|
|
7
|
+
if(btn.id === 'btn-deploy'){
|
|
6
8
|
path = '/deploy';
|
|
7
|
-
}else if(
|
|
9
|
+
}else if(btn.id === 'btn-rebuild'){
|
|
8
10
|
path = '/rebuild';
|
|
9
11
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
})
|
|
16
|
-
.done(function( data ) {
|
|
17
|
-
$('#waiting').hide();
|
|
18
|
-
return false;
|
|
12
|
+
document.getElementById('waiting').style.display = 'block';
|
|
13
|
+
fetch(path, { method: 'POST' })
|
|
14
|
+
.finally(function() {
|
|
15
|
+
document.getElementById('waiting').style.display = 'none';
|
|
19
16
|
});
|
|
20
17
|
});
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
document.addEventListener('submit', function(e){
|
|
19
|
+
var form = e.target.closest('.form-confirm');
|
|
20
|
+
if(!form) return;
|
|
21
|
+
if(!window.confirm(form.getAttribute('data-confirm'))){
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
}
|
|
23
24
|
});
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
// Autosave
|
|
27
|
+
var autosaveForm = document.querySelector('form[data-autosave-key]');
|
|
28
|
+
if (autosaveForm) {
|
|
29
|
+
var autosaveKey = autosaveForm.getAttribute('data-autosave-key');
|
|
30
|
+
var autosaveInterval = 15000;
|
|
31
|
+
var autosaveTimer = null;
|
|
32
|
+
var autosaveFields = ['i-title', 'i-date', 'i-tags'];
|
|
33
|
+
|
|
34
|
+
function autosaveGetContent() {
|
|
35
|
+
if (window.myCodeMirror) return window.myCodeMirror.getValue();
|
|
36
|
+
var ta = document.getElementById('i-content');
|
|
37
|
+
return ta ? ta.value : '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function autosaveSetContent(val) {
|
|
41
|
+
if (window.myCodeMirror) { window.myCodeMirror.setValue(val); return; }
|
|
42
|
+
var ta = document.getElementById('i-content');
|
|
43
|
+
if (ta) ta.value = val;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function autosaveCollect() {
|
|
47
|
+
var data = { _ts: Date.now() };
|
|
48
|
+
autosaveFields.forEach(function(id) {
|
|
49
|
+
var el = document.getElementById(id);
|
|
50
|
+
if (el) data[id] = el.value;
|
|
51
|
+
});
|
|
52
|
+
var layoutEl = autosaveForm.querySelector('select[name="layout"]');
|
|
53
|
+
if (layoutEl) data['layout'] = layoutEl.value;
|
|
54
|
+
data['content'] = autosaveGetContent();
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function autosaveSave() {
|
|
59
|
+
try {
|
|
60
|
+
localStorage.setItem(autosaveKey, JSON.stringify(autosaveCollect()));
|
|
61
|
+
var ind = document.getElementById('autosave-indicator');
|
|
62
|
+
if (ind) {
|
|
63
|
+
ind.classList.add('visible');
|
|
64
|
+
setTimeout(function() { ind.classList.remove('visible'); }, 2000);
|
|
65
|
+
}
|
|
66
|
+
} catch(e) {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function autosaveRestore(data) {
|
|
70
|
+
autosaveFields.forEach(function(id) {
|
|
71
|
+
var el = document.getElementById(id);
|
|
72
|
+
if (el && data[id] !== undefined) el.value = data[id];
|
|
73
|
+
});
|
|
74
|
+
var layoutEl = autosaveForm.querySelector('select[name="layout"]');
|
|
75
|
+
if (layoutEl && data['layout']) layoutEl.value = data['layout'];
|
|
76
|
+
if (data['content'] !== undefined) autosaveSetContent(data['content']);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function autosaveShowBanner(data) {
|
|
80
|
+
var banner = document.getElementById('autosave-banner');
|
|
81
|
+
if (!banner) return;
|
|
82
|
+
var dateStr = new Date(data._ts).toLocaleString();
|
|
83
|
+
document.getElementById('autosave-banner-text').textContent =
|
|
84
|
+
'Autosaved draft from ' + dateStr;
|
|
85
|
+
banner.classList.remove('d-none');
|
|
86
|
+
|
|
87
|
+
document.getElementById('autosave-restore').addEventListener('click', function() {
|
|
88
|
+
autosaveRestore(data);
|
|
89
|
+
banner.classList.add('d-none');
|
|
90
|
+
});
|
|
91
|
+
document.getElementById('autosave-discard').addEventListener('click', function() {
|
|
92
|
+
localStorage.removeItem(autosaveKey);
|
|
93
|
+
banner.classList.add('d-none');
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function autosaveInit() {
|
|
98
|
+
try {
|
|
99
|
+
var raw = localStorage.getItem(autosaveKey);
|
|
100
|
+
if (raw) {
|
|
101
|
+
var data = JSON.parse(raw);
|
|
102
|
+
if (Date.now() - data._ts < 7 * 24 * 3600 * 1000) {
|
|
103
|
+
autosaveShowBanner(data);
|
|
104
|
+
} else {
|
|
105
|
+
localStorage.removeItem(autosaveKey);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch(e) { localStorage.removeItem(autosaveKey); }
|
|
109
|
+
|
|
110
|
+
autosaveTimer = setInterval(autosaveSave, autosaveInterval);
|
|
111
|
+
|
|
112
|
+
autosaveForm.addEventListener('submit', function() {
|
|
113
|
+
localStorage.removeItem(autosaveKey);
|
|
114
|
+
if (autosaveTimer) clearInterval(autosaveTimer);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Wait for CodeMirror to be ready
|
|
119
|
+
(function waitCM(attempts) {
|
|
120
|
+
if (window.myCodeMirror || attempts <= 0) {
|
|
121
|
+
autosaveInit();
|
|
122
|
+
} else {
|
|
123
|
+
setTimeout(function() { waitCM(attempts - 1); }, 100);
|
|
124
|
+
}
|
|
125
|
+
})(20);
|
|
126
|
+
}
|
|
127
|
+
});
|
data/bin/i18n/en.yml
CHANGED
|
@@ -44,6 +44,17 @@ deploy_dest_path: deploy remote path
|
|
|
44
44
|
help_deploy_dest_path: remote path on disk for ssh deployment
|
|
45
45
|
rsync_fullpath: rsync path
|
|
46
46
|
help_rsync_fullpath: if rsync is not in PATH, othervise just let 'rsync'
|
|
47
|
+
deploy_sftp: deploy SFTP
|
|
48
|
+
sftp_user: SFTP user
|
|
49
|
+
help_sftp_user: SFTP user for connection
|
|
50
|
+
sftp_host: SFTP host
|
|
51
|
+
help_sftp_host: SFTP server hostname or IP address
|
|
52
|
+
sftp_port: SFTP port
|
|
53
|
+
help_sftp_port: SFTP port (default 22)
|
|
54
|
+
sftp_dest_path: SFTP remote path
|
|
55
|
+
help_sftp_dest_path: remote path on the SFTP server for deployment
|
|
56
|
+
sftp_password: SFTP password
|
|
57
|
+
help_sftp_password: SFTP password (leave empty to use SSH key authentication)
|
|
47
58
|
site_index: index file name of site
|
|
48
59
|
help_site_index: link to open when we click on overview
|
|
49
60
|
hyde_admin_language: hyde admin language
|
|
@@ -110,4 +121,12 @@ help_resize_size: Downsize to fit to the specified size (resize preserve ratio),
|
|
|
110
121
|
resize_enable: Enable resizing for images
|
|
111
122
|
help_resize_enable: enable feature resize image at upload
|
|
112
123
|
text_editor_maximized: enable maximised editor
|
|
113
|
-
help_text_editor_maximized:
|
|
124
|
+
help_text_editor_maximized: expand the editor to the maximum of the content
|
|
125
|
+
global_search: search
|
|
126
|
+
global_search_placeholder: search posts, pages, files...
|
|
127
|
+
global_search_no_results: no results
|
|
128
|
+
global_search_results_for: results for
|
|
129
|
+
editor_preview: Preview
|
|
130
|
+
preview_building: Building preview...
|
|
131
|
+
preview_close: Close preview
|
|
132
|
+
preview_refresh: Refresh preview
|
data/bin/i18n/fr.yml
CHANGED
|
@@ -44,6 +44,17 @@ deploy_dest_path: chemin distant pour déploiement
|
|
|
44
44
|
help_deploy_dest_path: chemin distant pour déploiement du site
|
|
45
45
|
rsync_fullpath: chemin rsync
|
|
46
46
|
help_rsync_fullpath: si rsync n'est pas dans le PATH, sinon laissez 'rsync'
|
|
47
|
+
deploy_sftp: déployer SFTP
|
|
48
|
+
sftp_user: utilisateur SFTP
|
|
49
|
+
help_sftp_user: utilisateur pour la connexion SFTP
|
|
50
|
+
sftp_host: hôte SFTP
|
|
51
|
+
help_sftp_host: nom de domaine ou adresse IP du serveur SFTP
|
|
52
|
+
sftp_port: port SFTP
|
|
53
|
+
help_sftp_port: port SFTP (par défaut 22)
|
|
54
|
+
sftp_dest_path: chemin distant SFTP
|
|
55
|
+
help_sftp_dest_path: chemin distant sur le serveur SFTP pour le déploiement
|
|
56
|
+
sftp_password: mot de passe SFTP
|
|
57
|
+
help_sftp_password: mot de passe SFTP (laisser vide pour utiliser l'authentification par clé SSH)
|
|
47
58
|
site_index: nom du fichier index pour le site
|
|
48
59
|
help_site_index: Lien pour le lien aperçu, pensez à l'extention si nécessaire
|
|
49
60
|
hyde_admin_language: hyde admin langue
|
|
@@ -91,7 +102,7 @@ parent_dir: dossier parent
|
|
|
91
102
|
sort_by_date: tri par date
|
|
92
103
|
older: vieille d'abord
|
|
93
104
|
newer: récente d'abord
|
|
94
|
-
previous_images: images précédentes
|
|
105
|
+
previous_images: images précédentes
|
|
95
106
|
next_images: images suivantes
|
|
96
107
|
search: recherche
|
|
97
108
|
filename: nom de fichier
|
|
@@ -110,4 +121,12 @@ help_resize_size: Réduit les image pour coller à la taille spécifiée (le red
|
|
|
110
121
|
resize_enable: activer redimensionnement
|
|
111
122
|
help_resize_enable: active la fonctionnalité de redimensionnement d'image à l'upload
|
|
112
123
|
text_editor_maximized: activer l'editeur maximisé
|
|
113
|
-
help_text_editor_maximized: permet d'agrandir l'éditeur au maximum du contenu
|
|
124
|
+
help_text_editor_maximized: permet d'agrandir l'éditeur au maximum du contenu
|
|
125
|
+
global_search: recherche
|
|
126
|
+
global_search_placeholder: rechercher articles, pages, fichiers...
|
|
127
|
+
global_search_no_results: aucun résultat
|
|
128
|
+
global_search_results_for: résultats pour
|
|
129
|
+
editor_preview: Aperçu
|
|
130
|
+
preview_building: Génération de l'aperçu...
|
|
131
|
+
preview_close: Fermer l'aperçu
|
|
132
|
+
preview_refresh: Rafraîchir l'aperçu
|
data/hyde_admin.gemspec
CHANGED
|
@@ -25,14 +25,12 @@ Gem::Specification.new do |s|
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
s.license = 'MIT'
|
|
28
|
-
s.add_runtime_dependency("roda", "~> 3.48
|
|
29
|
-
s.add_runtime_dependency("roda-i18n", "~> 0.4
|
|
30
|
-
s.add_runtime_dependency("roda-http-auth", "0.2
|
|
31
|
-
s.add_runtime_dependency(
|
|
32
|
-
|
|
33
|
-
s.add_runtime_dependency('jekyll') # Because we call jekyll binary
|
|
34
|
-
s.add_runtime_dependency('image_processing') # JPEG quality
|
|
28
|
+
s.add_runtime_dependency("roda", "~> 3.48")
|
|
29
|
+
s.add_runtime_dependency("roda-i18n", "~> 0.4")
|
|
30
|
+
s.add_runtime_dependency("roda-http-auth", "~> 0.2")
|
|
31
|
+
s.add_runtime_dependency('jekyll')
|
|
32
|
+
s.add_runtime_dependency('image_processing')
|
|
35
33
|
s.add_runtime_dependency('mini_magick')
|
|
36
|
-
s.add_runtime_dependency('rack',
|
|
34
|
+
s.add_runtime_dependency('rack', "~> 2.2")
|
|
37
35
|
s.add_runtime_dependency('puma')
|
|
38
36
|
end
|
data/lib/hyde_admin/version.rb
CHANGED