servel 0.19.0 → 0.20.0
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/lib/servel.rb +2 -0
- data/lib/servel/app.rb +10 -1
- data/lib/servel/entries.rb +68 -0
- data/lib/servel/entry.rb +44 -26
- data/lib/servel/haml_context.rb +37 -53
- data/lib/servel/index.rb +4 -63
- data/lib/servel/templates/_listing.haml +7 -15
- data/lib/servel/templates/css/common.css +15 -0
- data/lib/servel/templates/css/gallery.css +3 -1
- data/lib/servel/templates/css/listing.css +30 -5
- data/lib/servel/templates/index.haml +4 -4
- data/lib/servel/templates/js/gallery.js +41 -36
- data/lib/servel/templates/js/index.js +54 -0
- data/lib/servel/templates/js/listing.js +161 -26
- data/lib/servel/version.rb +1 -1
- metadata +4 -3
- data/lib/servel/templates/js/url-search-params.js +0 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2763001e5065c946d727b67fdf97e42a888d6794159528c72b2f15c7492c3409
|
4
|
+
data.tar.gz: d44b73a268aa7e69ea9b431ade3700a311b37906614fd8601effafc09473b2f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea0615e5c1661f40afd3ebf639e6d751db50989506fec1434c81f72ca3aaa3107c0eb5a8aa5f40e899e7d94ca62c99332e0998389c4a23296c910f8dd2c43750
|
7
|
+
data.tar.gz: 0071e811b8c7c5a401b5a060c297a686616e311a9bd6726a4e4bd5d6ed6626ebf25cd193ea2202dbeaff9a9ca851bfc4f19de73ad819a5245e33c196cb0c3ede
|
data/lib/servel.rb
CHANGED
@@ -7,6 +7,7 @@ require 'lru_redux'
|
|
7
7
|
|
8
8
|
require 'thread'
|
9
9
|
require 'pathname'
|
10
|
+
require 'json'
|
10
11
|
|
11
12
|
module Servel
|
12
13
|
def self.build_app(path_map)
|
@@ -22,6 +23,7 @@ require "servel/instrumentation"
|
|
22
23
|
require "servel/entry"
|
23
24
|
require "servel/entry_factory"
|
24
25
|
require "servel/haml_context"
|
26
|
+
require "servel/entries"
|
25
27
|
require "servel/index"
|
26
28
|
require "servel/app"
|
27
29
|
require "servel/home_app"
|
data/lib/servel/app.rb
CHANGED
@@ -26,7 +26,12 @@ class Servel::App
|
|
26
26
|
return [404, {}, []] unless fs_path.exist?
|
27
27
|
|
28
28
|
request = Rack::Request.new(env)
|
29
|
-
|
29
|
+
|
30
|
+
if json_request?(request)
|
31
|
+
Servel::Entries.new(url_root: url_root, url_path: url_path, fs_path: fs_path, params: request.params).render
|
32
|
+
else
|
33
|
+
Servel::Index.new(url_root: url_root, url_path: url_path).render
|
34
|
+
end
|
30
35
|
end
|
31
36
|
|
32
37
|
def redirect(location)
|
@@ -39,6 +44,10 @@ class Servel::App
|
|
39
44
|
Rack::Utils.clean_path_info(url_path)
|
40
45
|
end
|
41
46
|
|
47
|
+
def json_request?(request)
|
48
|
+
Rack::Utils.best_q_match(request.get_header("HTTP_ACCEPT"), ['text/html', 'application/json']) == 'application/json'
|
49
|
+
end
|
50
|
+
|
42
51
|
def try_encode(string)
|
43
52
|
return string if string.encoding == UTF_8
|
44
53
|
string.encode(UTF_8)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class Servel::Entries
|
2
|
+
extend Servel::Instrumentation
|
3
|
+
RENDER_CACHE = LruRedux::ThreadSafeCache.new(100)
|
4
|
+
SORT_METHODS = ["name", "mtime", "size", "type"]
|
5
|
+
SORT_DIRECTIONS = ["asc", "desc"]
|
6
|
+
|
7
|
+
def initialize(url_root:, url_path:, fs_path:, params:)
|
8
|
+
@url_root = url_root
|
9
|
+
@url_path = url_path
|
10
|
+
@fs_path = fs_path
|
11
|
+
@params = params
|
12
|
+
end
|
13
|
+
|
14
|
+
def render
|
15
|
+
RENDER_CACHE.getset(render_cache_key) { [200, {}, [entries.to_json]] }
|
16
|
+
end
|
17
|
+
|
18
|
+
def render_cache_key
|
19
|
+
@render_cache_key ||= [@fs_path.to_s, @fs_path.mtime.to_i, sort_method, sort_direction].join("-")
|
20
|
+
end
|
21
|
+
|
22
|
+
def entries
|
23
|
+
children = @fs_path.children.map { |path| Servel::EntryFactory.for(path) }.compact
|
24
|
+
special_entries + apply_sort(children.select(&:directory?)) + apply_sort(children.select(&:file?))
|
25
|
+
end
|
26
|
+
|
27
|
+
def sort_method
|
28
|
+
param = @params["_servel_sort_method"]
|
29
|
+
param = "name" unless SORT_METHODS.include?(param)
|
30
|
+
param
|
31
|
+
end
|
32
|
+
|
33
|
+
def sort_direction
|
34
|
+
param = @params["_servel_sort_direction"]
|
35
|
+
param = "asc" unless SORT_DIRECTIONS.include?(param)
|
36
|
+
param
|
37
|
+
end
|
38
|
+
|
39
|
+
def special_entries
|
40
|
+
list = []
|
41
|
+
list << Servel::EntryFactory.home("/") if @url_root != ""
|
42
|
+
|
43
|
+
unless @url_path == "/"
|
44
|
+
list << Servel::EntryFactory.top(@url_root == "" ? "/" : @url_root)
|
45
|
+
list << Servel::EntryFactory.parent("../")
|
46
|
+
end
|
47
|
+
|
48
|
+
list
|
49
|
+
end
|
50
|
+
|
51
|
+
def apply_sort(entries)
|
52
|
+
entries = case sort_method
|
53
|
+
when "name"
|
54
|
+
Naturalsorter::Sorter.sort_by_method(entries, :name, true)
|
55
|
+
when "mtime"
|
56
|
+
entries.sort_by { |entry| entry.mtime }
|
57
|
+
when "size"
|
58
|
+
entries.sort_by { |entry| entry.size || 0 }
|
59
|
+
when "type"
|
60
|
+
entries.sort_by { |entry| entry.type }
|
61
|
+
end
|
62
|
+
|
63
|
+
entries.reverse! if sort_direction == "desc"
|
64
|
+
entries
|
65
|
+
end
|
66
|
+
|
67
|
+
instrument :render, :entries, :apply_sort
|
68
|
+
end
|
data/lib/servel/entry.rb
CHANGED
@@ -1,27 +1,45 @@
|
|
1
|
-
class Servel::Entry
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
1
|
+
class Servel::Entry
|
2
|
+
extend Servel::Instrumentation
|
3
|
+
|
4
|
+
attr_reader :ftype, :type, :media_type, :listing_classes, :icon, :href, :name, :size, :mtime
|
5
|
+
|
6
|
+
def initialize(ftype:, type:, media_type: nil, listing_classes:, icon:, href:, name:, size: nil, mtime: nil)
|
7
|
+
@ftype = ftype
|
8
|
+
@type = type
|
9
|
+
@media_type = media_type
|
10
|
+
@listing_classes = listing_classes
|
11
|
+
@icon = icon
|
12
|
+
@href = href
|
13
|
+
@name = name
|
14
|
+
@size = size
|
15
|
+
@mtime = mtime
|
16
|
+
end
|
17
|
+
|
18
|
+
def directory?
|
19
|
+
@ftype == :directory
|
20
|
+
end
|
21
|
+
|
22
|
+
def file?
|
23
|
+
@ftype == :file
|
24
|
+
end
|
25
|
+
|
26
|
+
def media?
|
27
|
+
!@media_type.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def as_json(*)
|
31
|
+
{
|
32
|
+
icon: @icon,
|
33
|
+
href: Rack::Utils.escape_path(@href),
|
34
|
+
class: @listing_classes,
|
35
|
+
media_type: @media_type,
|
36
|
+
name: @name,
|
37
|
+
type: @type,
|
38
|
+
size: @size.nil? ? "-" : @size,
|
39
|
+
mtime: @mtime.nil? ? "-" : @mtime.strftime("%e %b %Y %l:%M %p"),
|
40
|
+
media: media?
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
instrument :as_json
|
27
45
|
end
|
data/lib/servel/haml_context.rb
CHANGED
@@ -1,54 +1,38 @@
|
|
1
|
-
class Servel::HamlContext
|
2
|
-
extend Servel::Instrumentation
|
3
|
-
include ActiveSupport::NumberHelper
|
4
|
-
|
5
|
-
LOCK = Mutex.new
|
6
|
-
|
7
|
-
def self.render(template, locals)
|
8
|
-
[200, {}, [new.render(template, locals)]]
|
9
|
-
end
|
10
|
-
|
11
|
-
def initialize
|
12
|
-
@build_path = Pathname.new(__FILE__).dirname.realpath + 'templates'
|
13
|
-
end
|
14
|
-
|
15
|
-
def render(template, locals = {})
|
16
|
-
haml_engine(template).render(self, locals)
|
17
|
-
end
|
18
|
-
|
19
|
-
def partial(name, locals = {})
|
20
|
-
render("_#{name}.haml", locals)
|
21
|
-
end
|
22
|
-
|
23
|
-
def include(path)
|
24
|
-
(@build_path + path).read
|
25
|
-
end
|
26
|
-
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
data: data,
|
39
|
-
class: classes
|
40
|
-
}
|
41
|
-
end
|
42
|
-
|
43
|
-
def haml_engine(path)
|
44
|
-
LOCK.synchronize do
|
45
|
-
@@haml_engine_cache ||= {}
|
46
|
-
unless @@haml_engine_cache.key?(path)
|
47
|
-
@@haml_engine_cache[path] = Hamlit::Template.new(filename: path) { include(path) }
|
48
|
-
end
|
49
|
-
@@haml_engine_cache[path]
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
instrument :render, :partial, :include
|
1
|
+
class Servel::HamlContext
|
2
|
+
extend Servel::Instrumentation
|
3
|
+
include ActiveSupport::NumberHelper
|
4
|
+
|
5
|
+
LOCK = Mutex.new
|
6
|
+
|
7
|
+
def self.render(template, locals)
|
8
|
+
[200, {}, [new.render(template, locals)]]
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@build_path = Pathname.new(__FILE__).dirname.realpath + 'templates'
|
13
|
+
end
|
14
|
+
|
15
|
+
def render(template, locals = {})
|
16
|
+
haml_engine(template).render(self, locals)
|
17
|
+
end
|
18
|
+
|
19
|
+
def partial(name, locals = {})
|
20
|
+
render("_#{name}.haml", locals)
|
21
|
+
end
|
22
|
+
|
23
|
+
def include(path)
|
24
|
+
(@build_path + path).read
|
25
|
+
end
|
26
|
+
|
27
|
+
def haml_engine(path)
|
28
|
+
LOCK.synchronize do
|
29
|
+
@@haml_engine_cache ||= {}
|
30
|
+
unless @@haml_engine_cache.key?(path)
|
31
|
+
@@haml_engine_cache[path] = Hamlit::Template.new(filename: path) { include(path) }
|
32
|
+
end
|
33
|
+
@@haml_engine_cache[path]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
instrument :render, :partial, :include
|
54
38
|
end
|
data/lib/servel/index.rb
CHANGED
@@ -1,80 +1,21 @@
|
|
1
1
|
class Servel::Index
|
2
2
|
extend Servel::Instrumentation
|
3
|
-
RENDER_CACHE = LruRedux::ThreadSafeCache.new(100)
|
4
|
-
SORT_METHODS = ["name", "mtime", "size", "type"]
|
5
|
-
SORT_DIRECTIONS = ["asc", "desc"]
|
6
3
|
|
7
|
-
def initialize(url_root:, url_path
|
4
|
+
def initialize(url_root:, url_path:)
|
8
5
|
@url_root = url_root
|
9
6
|
@url_path = url_path
|
10
|
-
@fs_path = fs_path
|
11
|
-
@params = params
|
12
7
|
end
|
13
8
|
|
14
9
|
def render
|
15
|
-
|
16
|
-
end
|
17
|
-
|
18
|
-
def render_cache_key
|
19
|
-
@render_cache_key ||= [@fs_path.to_s, @fs_path.mtime.to_i, sort_method, sort_direction].join("-")
|
10
|
+
Servel::HamlContext.render('index.haml', locals)
|
20
11
|
end
|
21
12
|
|
22
13
|
def locals
|
23
14
|
{
|
24
15
|
url_root: @url_root,
|
25
|
-
url_path: @url_path
|
26
|
-
entries: entries,
|
27
|
-
sort: {
|
28
|
-
method: sort_method,
|
29
|
-
direction: sort_direction
|
30
|
-
}
|
16
|
+
url_path: @url_path
|
31
17
|
}
|
32
18
|
end
|
33
19
|
|
34
|
-
|
35
|
-
children = @fs_path.children.map { |path| Servel::EntryFactory.for(path) }.compact
|
36
|
-
special_entries + apply_sort(children.select(&:directory?)) + apply_sort(children.select(&:file?))
|
37
|
-
end
|
38
|
-
|
39
|
-
def sort_method
|
40
|
-
param = @params["_servel_sort_method"]
|
41
|
-
param = "name" unless SORT_METHODS.include?(param)
|
42
|
-
param
|
43
|
-
end
|
44
|
-
|
45
|
-
def sort_direction
|
46
|
-
param = @params["_servel_sort_direction"]
|
47
|
-
param = "asc" unless SORT_DIRECTIONS.include?(param)
|
48
|
-
param
|
49
|
-
end
|
50
|
-
|
51
|
-
def special_entries
|
52
|
-
list = []
|
53
|
-
list << Servel::EntryFactory.home("/") if @url_root != ""
|
54
|
-
|
55
|
-
unless @url_path == "/"
|
56
|
-
list << Servel::EntryFactory.top(@url_root == "" ? "/" : @url_root)
|
57
|
-
list << Servel::EntryFactory.parent("../")
|
58
|
-
end
|
59
|
-
|
60
|
-
list
|
61
|
-
end
|
62
|
-
|
63
|
-
def apply_sort(entries)
|
64
|
-
entries = case sort_method
|
65
|
-
when "name"
|
66
|
-
Naturalsorter::Sorter.sort_by_method(entries, :name, true)
|
67
|
-
when "mtime"
|
68
|
-
entries.sort_by { |entry| entry.mtime }
|
69
|
-
when "size"
|
70
|
-
entries.sort_by { |entry| entry.size || 0 }
|
71
|
-
when "type"
|
72
|
-
entries.sort_by { |entry| entry.type }
|
73
|
-
end
|
74
|
-
|
75
|
-
entries.reverse! if sort_direction == "desc"
|
76
|
-
entries
|
77
|
-
end
|
78
|
-
|
79
|
-
instrument :render, :locals, :entries, :apply_sort
|
20
|
+
instrument :render, :locals
|
80
21
|
end
|
@@ -1,25 +1,17 @@
|
|
1
1
|
%h1 Listing of #{url_root}#{url_path}
|
2
|
+
#search-wrapper
|
3
|
+
%input#search{type: 'search'}
|
2
4
|
.table-wrapper
|
3
5
|
%table
|
4
6
|
%thead
|
5
7
|
%tr
|
6
|
-
%th{
|
8
|
+
%th.sortable.sort-active.sort-asc{data: { sort_method: 'name', sort_direction: 'asc' }}
|
7
9
|
%a{href: "#"} Name
|
8
10
|
%th.new-tab
|
9
|
-
%th.type{
|
11
|
+
%th.type.sortable{data: { sort_method: 'type', sort_direction: 'asc'}}
|
10
12
|
%a{href: "#"} Type
|
11
|
-
%th.size{
|
13
|
+
%th.size.sortable{data: { sort_method: 'size', sort_direction: 'asc'}}
|
12
14
|
%a{href: "#"} Size
|
13
|
-
%th.modified{
|
15
|
+
%th.modified.sortable{data: { sort_method: 'mtime', sort_direction: 'asc'}}
|
14
16
|
%a{href: "#"} Modified
|
15
|
-
|
16
|
-
- entries.each do |file|
|
17
|
-
%tr
|
18
|
-
%td
|
19
|
-
%span.icon= file.icon
|
20
|
-
%a.default{href: Rack::Utils.escape_path(file.href), class: file.listing_classes, data: { type: file.media_type }}= file.name
|
21
|
-
%td
|
22
|
-
%a.new-tab{href: Rack::Utils.escape_path(file.href), class: file.listing_classes, target: "_blank"} (New tab)
|
23
|
-
%td= file.type
|
24
|
-
%td= file.size.nil? ? "-" : number_to_human_size(file.size)
|
25
|
-
%td= file.mtime.nil? ? "-" : file.mtime.strftime("%e %b %Y %l:%M %p")
|
17
|
+
#listing-container
|
@@ -22,4 +22,19 @@ a {
|
|
22
22
|
|
23
23
|
a:hover, a:focus, a:active {
|
24
24
|
text-decoration: underline;
|
25
|
+
}
|
26
|
+
|
27
|
+
#loading {
|
28
|
+
display: flex;
|
29
|
+
align-items: center;
|
30
|
+
justify-content: center;
|
31
|
+
position: fixed;
|
32
|
+
top: 0;
|
33
|
+
right: 0;
|
34
|
+
bottom: 0;
|
35
|
+
left: 0;
|
36
|
+
z-index: 2;
|
37
|
+
background-color: rgba(255, 255, 255, 0.6);
|
38
|
+
font-size: 24px;
|
39
|
+
font-weight: bold;
|
25
40
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
#gallery {
|
2
2
|
background: #333;
|
3
|
-
display:
|
3
|
+
display: none;
|
4
4
|
align-items: center;
|
5
5
|
justify-content: center;
|
6
6
|
}
|
@@ -49,6 +49,7 @@
|
|
49
49
|
top: 0;
|
50
50
|
right: 0;
|
51
51
|
left: 0;
|
52
|
+
pointer-events: none;
|
52
53
|
}
|
53
54
|
|
54
55
|
.paginator {
|
@@ -63,6 +64,7 @@
|
|
63
64
|
display: flex;
|
64
65
|
align-items: center;
|
65
66
|
justify-content: center;
|
67
|
+
pointer-events: auto;
|
66
68
|
}
|
67
69
|
|
68
70
|
.paginator:active {
|
@@ -1,4 +1,5 @@
|
|
1
1
|
#listing {
|
2
|
+
display: none;
|
2
3
|
padding: 10px;
|
3
4
|
position: relative;
|
4
5
|
z-index: 1;
|
@@ -9,6 +10,21 @@
|
|
9
10
|
margin-top: 0;
|
10
11
|
}
|
11
12
|
|
13
|
+
#search-wrapper {
|
14
|
+
text-align: right;
|
15
|
+
margin-bottom: 20px;
|
16
|
+
}
|
17
|
+
|
18
|
+
#search {
|
19
|
+
width: 300px;
|
20
|
+
max-width: 100%;
|
21
|
+
padding: 5px;
|
22
|
+
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
|
23
|
+
font-size: 16px;
|
24
|
+
line-height: 1.4;
|
25
|
+
border: 1px solid #ddd;
|
26
|
+
}
|
27
|
+
|
12
28
|
.table-wrapper {
|
13
29
|
overflow: auto;
|
14
30
|
}
|
@@ -19,6 +35,11 @@ table {
|
|
19
35
|
border-collapse: collapse;
|
20
36
|
border: 1px solid #ddd;
|
21
37
|
table-layout: fixed;
|
38
|
+
will-change: transform;
|
39
|
+
}
|
40
|
+
|
41
|
+
#listing-container table, #listing-container tr:first-child td {
|
42
|
+
border-top: none;
|
22
43
|
}
|
23
44
|
|
24
45
|
th {
|
@@ -30,23 +51,27 @@ th, td {
|
|
30
51
|
border: 1px solid #ddd;
|
31
52
|
}
|
32
53
|
|
54
|
+
tbody {
|
55
|
+
vertical-align: top;
|
56
|
+
}
|
57
|
+
|
33
58
|
tbody > tr:nth-of-type(odd) {
|
34
59
|
background-color: #f9f9f9;
|
35
60
|
}
|
36
61
|
|
37
|
-
th.new-tab {
|
62
|
+
th.new-tab, td.new-tab {
|
38
63
|
width: 6em;
|
39
64
|
}
|
40
65
|
|
41
|
-
th.type {
|
42
|
-
width:
|
66
|
+
th.type, td.type {
|
67
|
+
width: 5em;
|
43
68
|
}
|
44
69
|
|
45
|
-
th.size {
|
70
|
+
th.size, td.size {
|
46
71
|
width: 6em;
|
47
72
|
}
|
48
73
|
|
49
|
-
th.modified {
|
74
|
+
th.modified, td.modified {
|
50
75
|
width: 12em;
|
51
76
|
}
|
52
77
|
|
@@ -12,11 +12,11 @@
|
|
12
12
|
#{include('css/gallery-text.css')}
|
13
13
|
|
14
14
|
:javascript
|
15
|
-
#{include('js/url-search-params.js')}
|
16
15
|
#{include('js/listing.js')}
|
17
16
|
#{include('js/ume.js')}
|
18
17
|
#{include('js/gallery.js')}
|
18
|
+
#{include('js/index.js')}
|
19
19
|
%body
|
20
|
-
|
21
|
-
|
22
|
-
#
|
20
|
+
#gallery!= partial('gallery')
|
21
|
+
#listing!= partial('listing', { url_root: url_root, url_path: url_path })
|
22
|
+
#loading Loading...
|
@@ -1,21 +1,11 @@
|
|
1
|
-
|
1
|
+
"use strict";
|
2
2
|
|
3
3
|
var Gallery = (function() {
|
4
|
-
var
|
5
|
-
var
|
4
|
+
var $;
|
5
|
+
var $gallery;
|
6
|
+
var mediaEntries = [];
|
6
7
|
var currentIndex = 0;
|
7
8
|
var layoutItemMax = false;
|
8
|
-
var $gallery;
|
9
|
-
|
10
|
-
function initItems() {
|
11
|
-
var links = document.querySelectorAll("#listing a.default.media");
|
12
|
-
for(var i = 0; i < links.length; i++) {
|
13
|
-
var link = links[i];
|
14
|
-
|
15
|
-
urls.push(link.href);
|
16
|
-
types.push(link.dataset.type);
|
17
|
-
}
|
18
|
-
}
|
19
9
|
|
20
10
|
function renderText(url) {
|
21
11
|
var http = new XMLHttpRequest();
|
@@ -39,8 +29,10 @@ var Gallery = (function() {
|
|
39
29
|
function render() {
|
40
30
|
clearContent();
|
41
31
|
|
42
|
-
var
|
43
|
-
|
32
|
+
var entry = mediaEntries[currentIndex];
|
33
|
+
|
34
|
+
var url = entry.href;
|
35
|
+
var type = entry.media_type;
|
44
36
|
|
45
37
|
$gallery.classList.add(type);
|
46
38
|
|
@@ -60,7 +52,7 @@ var Gallery = (function() {
|
|
60
52
|
|
61
53
|
function clamp(index) {
|
62
54
|
if(index == null || isNaN(index) || index < 0) return 0;
|
63
|
-
if(index >=
|
55
|
+
if(index >= mediaEntries.length) return mediaEntries.length - 1;
|
64
56
|
return index;
|
65
57
|
}
|
66
58
|
|
@@ -89,7 +81,10 @@ var Gallery = (function() {
|
|
89
81
|
}
|
90
82
|
|
91
83
|
function jump(url) {
|
92
|
-
|
84
|
+
var index = mediaEntries.findIndex(function(entry) {
|
85
|
+
return entry.href == url;
|
86
|
+
});
|
87
|
+
go(index);
|
93
88
|
}
|
94
89
|
|
95
90
|
function atBottom() {
|
@@ -102,7 +97,7 @@ var Gallery = (function() {
|
|
102
97
|
|
103
98
|
if(e.target.matches("a.media:not(.new-tab)")) {
|
104
99
|
e.preventDefault();
|
105
|
-
jump(e.target.
|
100
|
+
jump(e.target.dataset.url);
|
106
101
|
}
|
107
102
|
else if(e.target.matches("#page-back")) {
|
108
103
|
e.stopPropagation();
|
@@ -132,6 +127,8 @@ var Gallery = (function() {
|
|
132
127
|
});
|
133
128
|
|
134
129
|
window.addEventListener("keydown", function(e) {
|
130
|
+
if(e.target == $("#search")) return;
|
131
|
+
|
135
132
|
if(e.keyCode == 39 || ((e.keyCode == 32 || e.keyCode == 13) && atBottom())) {
|
136
133
|
e.preventDefault();
|
137
134
|
next();
|
@@ -159,19 +156,35 @@ var Gallery = (function() {
|
|
159
156
|
layout();
|
160
157
|
}
|
161
158
|
|
162
|
-
function
|
163
|
-
|
159
|
+
function onEntriesInit() {
|
160
|
+
onEntriesUpdate();
|
164
161
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
162
|
+
if(mediaEntries.length > 0) {
|
163
|
+
$gallery.style.display = "flex";
|
164
|
+
|
165
|
+
initEvents();
|
166
|
+
initLayout();
|
167
|
+
}
|
168
|
+
}
|
169
|
+
|
170
|
+
function onEntriesUpdate() {
|
171
|
+
currentIndex = 0;
|
172
|
+
mediaEntries = [];
|
173
|
+
for(var i = 0; i < window.entries.length; i++) {
|
174
|
+
if(window.entries[i].media) mediaEntries.push(window.entries[i]);
|
175
|
+
}
|
176
|
+
|
177
|
+
if(mediaEntries.length > 0) render();
|
169
178
|
}
|
170
179
|
|
180
|
+
window.addEventListener("DOMContentLoaded", function() {
|
181
|
+
$ = document.querySelector.bind(document);
|
182
|
+
$gallery = $("#gallery");
|
183
|
+
});
|
184
|
+
|
171
185
|
return {
|
172
|
-
|
173
|
-
|
174
|
-
clamp: clamp,
|
186
|
+
onEntriesInit: onEntriesInit,
|
187
|
+
onEntriesUpdate: onEntriesUpdate,
|
175
188
|
go: go,
|
176
189
|
prev: prev,
|
177
190
|
next: next,
|
@@ -180,11 +193,3 @@ var Gallery = (function() {
|
|
180
193
|
jump: jump
|
181
194
|
};
|
182
195
|
})();
|
183
|
-
|
184
|
-
function initGallery() {
|
185
|
-
$ = document.querySelector.bind(document);
|
186
|
-
if(!$("#gallery")) return;
|
187
|
-
Gallery.init();
|
188
|
-
}
|
189
|
-
|
190
|
-
window.addEventListener("DOMContentLoaded", initGallery);
|
@@ -0,0 +1,54 @@
|
|
1
|
+
"use strict";
|
2
|
+
|
3
|
+
var Index = (function() {
|
4
|
+
var $;
|
5
|
+
var inited = false;
|
6
|
+
|
7
|
+
function entriesURL() {
|
8
|
+
var sortable = $("th.sortable.sort-active");
|
9
|
+
return `${location.pathname}?_servel_sort_method=${sortable.dataset.sortMethod}&_servel_sort_direction=${sortable.dataset.sortDirection}`;
|
10
|
+
}
|
11
|
+
|
12
|
+
function onEntriesLoad(callback) {
|
13
|
+
if(inited) {
|
14
|
+
Gallery.onEntriesUpdate();
|
15
|
+
Listing.onEntriesUpdate();
|
16
|
+
}
|
17
|
+
else {
|
18
|
+
inited = true;
|
19
|
+
Gallery.onEntriesInit();
|
20
|
+
Listing.onEntriesInit();
|
21
|
+
}
|
22
|
+
|
23
|
+
$("#loading").style.display = "none";
|
24
|
+
if(callback) callback();
|
25
|
+
}
|
26
|
+
|
27
|
+
function loadEntries(callback) {
|
28
|
+
$("#loading").style.display = "flex";
|
29
|
+
|
30
|
+
var http = new XMLHttpRequest();
|
31
|
+
http.open("GET", entriesURL());
|
32
|
+
|
33
|
+
http.onreadystatechange = function() {
|
34
|
+
if(http.readyState === 4 && http.status === 200) {
|
35
|
+
window.entries = JSON.parse(http.responseText);
|
36
|
+
setTimeout(function() {
|
37
|
+
onEntriesLoad(callback);
|
38
|
+
}, 0);
|
39
|
+
}
|
40
|
+
};
|
41
|
+
|
42
|
+
http.setRequestHeader("Accept", "application/json");
|
43
|
+
http.send();
|
44
|
+
}
|
45
|
+
|
46
|
+
window.addEventListener("DOMContentLoaded", function() {
|
47
|
+
$ = document.querySelector.bind(document);
|
48
|
+
loadEntries();
|
49
|
+
});
|
50
|
+
|
51
|
+
return {
|
52
|
+
loadEntries: loadEntries
|
53
|
+
};
|
54
|
+
})();
|
@@ -1,26 +1,161 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
1
|
+
"use strict";
|
2
|
+
|
3
|
+
var Listing = (function() {
|
4
|
+
var $;
|
5
|
+
var $container;
|
6
|
+
var filteredEntries = [];
|
7
|
+
var perPage = 99;
|
8
|
+
var currentIndex = 0;
|
9
|
+
var moreContent = true;
|
10
|
+
var scrollDebounce = false;
|
11
|
+
|
12
|
+
function escapeHTML(unsafe) {
|
13
|
+
if(unsafe == null) return "";
|
14
|
+
return unsafe.toString()
|
15
|
+
.replace(/&/g, "&")
|
16
|
+
.replace(/</g, "<")
|
17
|
+
.replace(/>/g, ">")
|
18
|
+
.replace(/"/g, """)
|
19
|
+
.replace(/'/g, "'");
|
20
|
+
}
|
21
|
+
|
22
|
+
function HTMLSafe(pieces) {
|
23
|
+
var result = pieces[0];
|
24
|
+
var substitutions = [].slice.call(arguments, 1);
|
25
|
+
for(var i = 0; i < substitutions.length; ++i) result += escapeHTML(substitutions[i]) + pieces[i + 1];
|
26
|
+
return result;
|
27
|
+
}
|
28
|
+
|
29
|
+
function renderRow(file) {
|
30
|
+
return HTMLSafe`
|
31
|
+
<tr>
|
32
|
+
<td class="name">
|
33
|
+
<span class="icon">${file.icon}</span>
|
34
|
+
<a href="${file.href}" class="default ${file.class}" data-url="${file.href}" data-type="${file.media_type}">${file.name}</a>
|
35
|
+
</td>
|
36
|
+
<td class="new-tab">
|
37
|
+
<a href="${file.href}" class="new-tab ${file.class}" target="_blank">(New tab)</a>
|
38
|
+
</td>
|
39
|
+
<td class="type">${file.type}</td>
|
40
|
+
<td class="size">${file.size}</td>
|
41
|
+
<td class="modified">${file.mtime}</td>
|
42
|
+
</tr>
|
43
|
+
`;
|
44
|
+
}
|
45
|
+
|
46
|
+
function renderTable(currentEntries) {
|
47
|
+
var rows = currentEntries.map(function(entry) {
|
48
|
+
return renderRow(entry);
|
49
|
+
});
|
50
|
+
|
51
|
+
return `
|
52
|
+
<table>
|
53
|
+
<tbody>
|
54
|
+
${rows.join("")}
|
55
|
+
</tbody>
|
56
|
+
</table>
|
57
|
+
`;
|
58
|
+
}
|
59
|
+
|
60
|
+
function render() {
|
61
|
+
var currentEntries = filteredEntries.slice(currentIndex, currentIndex + perPage);
|
62
|
+
$container.insertAdjacentHTML("beforeend", renderTable(currentEntries));
|
63
|
+
}
|
64
|
+
|
65
|
+
function atBottom() {
|
66
|
+
return (window.scrollY + window.innerHeight) == document.body.scrollHeight;
|
67
|
+
}
|
68
|
+
|
69
|
+
function onScrolled() {
|
70
|
+
if(atBottom() && moreContent) {
|
71
|
+
currentIndex += perPage;
|
72
|
+
if(currentIndex >= filteredEntries.length) moreContent = false;
|
73
|
+
render();
|
74
|
+
}
|
75
|
+
scrollDebounce = false;
|
76
|
+
}
|
77
|
+
|
78
|
+
function applySort(sortable) {
|
79
|
+
var previousSortable = $("th.sortable.sort-active");
|
80
|
+
previousSortable.classList.remove("sort-active", "sort-asc", "sort-desc");
|
81
|
+
|
82
|
+
if(sortable == previousSortable) {
|
83
|
+
sortable.dataset.sortDirection = sortable.dataset.sortDirection == "asc" ? "desc" : "asc";
|
84
|
+
}
|
85
|
+
|
86
|
+
sortable.classList.add("sort-active", "sort-" + sortable.dataset.sortDirection);
|
87
|
+
|
88
|
+
Index.loadEntries(function() {
|
89
|
+
sortable.scrollIntoView();
|
90
|
+
});
|
91
|
+
}
|
92
|
+
|
93
|
+
function updateFilteredEntries(needle) {
|
94
|
+
if(needle == "") {
|
95
|
+
filteredEntries = window.entries;
|
96
|
+
}
|
97
|
+
else {
|
98
|
+
filteredEntries = [];
|
99
|
+
|
100
|
+
for(var i = 0; i < window.entries.length; i++) {
|
101
|
+
var entry = window.entries[i];
|
102
|
+
if(entry.name.toLowerCase().includes(needle.toLowerCase())) filteredEntries.push(entry);
|
103
|
+
}
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
function applyFilter(needle) {
|
108
|
+
updateFilteredEntries(needle);
|
109
|
+
|
110
|
+
$container.innerHTML = "";
|
111
|
+
currentIndex = 0;
|
112
|
+
moreContent = true;
|
113
|
+
render();
|
114
|
+
}
|
115
|
+
|
116
|
+
function initEvents() {
|
117
|
+
window.addEventListener("scroll", function(e) {
|
118
|
+
if(!scrollDebounce) {
|
119
|
+
scrollDebounce = true;
|
120
|
+
setTimeout(onScrolled, 0);
|
121
|
+
}
|
122
|
+
});
|
123
|
+
|
124
|
+
document.body.addEventListener("click", function(e) {
|
125
|
+
if(!e.target) return;
|
126
|
+
|
127
|
+
if(e.target.closest("th.sortable")) {
|
128
|
+
e.preventDefault();
|
129
|
+
applySort(e.target.closest("th.sortable"));
|
130
|
+
}
|
131
|
+
});
|
132
|
+
|
133
|
+
$("#search").addEventListener("keyup", function(e) {
|
134
|
+
e.stopPropagation();
|
135
|
+
|
136
|
+
if(e.keyCode == 13) {
|
137
|
+
applyFilter($("#search").value);
|
138
|
+
}
|
139
|
+
});
|
140
|
+
}
|
141
|
+
|
142
|
+
function onEntriesInit() {
|
143
|
+
onEntriesUpdate();
|
144
|
+
$("#listing").style.display = "block";
|
145
|
+
initEvents();
|
146
|
+
}
|
147
|
+
|
148
|
+
function onEntriesUpdate() {
|
149
|
+
applyFilter($("#search").value);
|
150
|
+
}
|
151
|
+
|
152
|
+
window.addEventListener("DOMContentLoaded", function() {
|
153
|
+
$ = document.querySelector.bind(document);
|
154
|
+
$container = $("#listing-container");
|
155
|
+
});
|
156
|
+
|
157
|
+
return {
|
158
|
+
onEntriesInit: onEntriesInit,
|
159
|
+
onEntriesUpdate: onEntriesUpdate
|
160
|
+
};
|
161
|
+
})();
|
data/lib/servel/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: servel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.20.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brenton "B-Train" Fletcher
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-08-
|
11
|
+
date: 2018-08-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -141,6 +141,7 @@ files:
|
|
141
141
|
- lib/servel.rb
|
142
142
|
- lib/servel/app.rb
|
143
143
|
- lib/servel/cli.rb
|
144
|
+
- lib/servel/entries.rb
|
144
145
|
- lib/servel/entry.rb
|
145
146
|
- lib/servel/entry_factory.rb
|
146
147
|
- lib/servel/haml_context.rb
|
@@ -158,9 +159,9 @@ files:
|
|
158
159
|
- lib/servel/templates/home.haml
|
159
160
|
- lib/servel/templates/index.haml
|
160
161
|
- lib/servel/templates/js/gallery.js
|
162
|
+
- lib/servel/templates/js/index.js
|
161
163
|
- lib/servel/templates/js/listing.js
|
162
164
|
- lib/servel/templates/js/ume.js
|
163
|
-
- lib/servel/templates/js/url-search-params.js
|
164
165
|
- lib/servel/version.rb
|
165
166
|
- servel.gemspec
|
166
167
|
homepage: http://bloople.net/
|
@@ -1,2 +0,0 @@
|
|
1
|
-
/*! (C) Andrea Giammarchi - Mit Style License */
|
2
|
-
var URLSearchParams=URLSearchParams||function(){"use strict";function URLSearchParams(query){var index,key,value,pairs,i,length,dict=Object.create(null);this[secret]=dict;if(!query)return;if(typeof query==="string"){if(query.charAt(0)==="?"){query=query.slice(1)}for(pairs=query.split("&"),i=0,length=pairs.length;i<length;i++){value=pairs[i];index=value.indexOf("=");if(-1<index){appendTo(dict,decode(value.slice(0,index)),decode(value.slice(index+1)))}else if(value.length){appendTo(dict,decode(value),"")}}}else{if(isArray(query)){for(i=0,length=query.length;i<length;i++){value=query[i];appendTo(dict,value[0],value[1])}}else{for(key in query){appendTo(dict,key,query[key])}}}}var isArray=Array.isArray,URLSearchParamsProto=URLSearchParams.prototype,find=/[!'\(\)~]|%20|%00/g,plus=/\+/g,replace={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"},replacer=function(match){return replace[match]},secret="__URLSearchParams__:"+Math.random();function appendTo(dict,name,value){if(name in dict){dict[name].push(""+value)}else{dict[name]=isArray(value)?value:[""+value]}}function decode(str){return decodeURIComponent(str.replace(plus," "))}function encode(str){return encodeURIComponent(str).replace(find,replacer)}URLSearchParamsProto.append=function append(name,value){appendTo(this[secret],name,value)};URLSearchParamsProto["delete"]=function del(name){delete this[secret][name]};URLSearchParamsProto.get=function get(name){var dict=this[secret];return name in dict?dict[name][0]:null};URLSearchParamsProto.getAll=function getAll(name){var dict=this[secret];return name in dict?dict[name].slice(0):[]};URLSearchParamsProto.has=function has(name){return name in this[secret]};URLSearchParamsProto.set=function set(name,value){this[secret][name]=[""+value]};URLSearchParamsProto.forEach=function forEach(callback,thisArg){var dict=this[secret];Object.getOwnPropertyNames(dict).forEach(function(name){dict[name].forEach(function(value){callback.call(thisArg,value,name,this)},this)},this)};URLSearchParamsProto.toJSON=function toJSON(){return{}};URLSearchParamsProto.toString=function toString(){var dict=this[secret],query=[],i,key,name,value;for(key in dict){name=encode(key);for(i=0,value=dict[key];i<value.length;i++){query.push(name+"="+encode(value[i]))}}return query.join("&")};var dP=Object.defineProperty,gOPD=Object.getOwnPropertyDescriptor,createSearchParamsPollute=function(search){function append(name,value){URLSearchParamsProto.append.call(this,name,value);name=this.toString();search.set.call(this._usp,name?"?"+name:"")}function del(name){URLSearchParamsProto["delete"].call(this,name);name=this.toString();search.set.call(this._usp,name?"?"+name:"")}function set(name,value){URLSearchParamsProto.set.call(this,name,value);name=this.toString();search.set.call(this._usp,name?"?"+name:"")}return function(sp,value){sp.append=append;sp["delete"]=del;sp.set=set;return dP(sp,"_usp",{configurable:true,writable:true,value:value})}},createSearchParamsCreate=function(polluteSearchParams){return function(obj,sp){dP(obj,"_searchParams",{configurable:true,writable:true,value:polluteSearchParams(sp,obj)});return sp}},updateSearchParams=function(sp){var append=sp.append;sp.append=URLSearchParamsProto.append;URLSearchParams.call(sp,sp._usp.search.slice(1));sp.append=append},verifySearchParams=function(obj,Class){if(!(obj instanceof Class))throw new TypeError("'searchParams' accessed on an object that "+"does not implement interface "+Class.name)},upgradeClass=function(Class){var ClassProto=Class.prototype,searchParams=gOPD(ClassProto,"searchParams"),href=gOPD(ClassProto,"href"),search=gOPD(ClassProto,"search"),createSearchParams;if(!searchParams&&search&&search.set){createSearchParams=createSearchParamsCreate(createSearchParamsPollute(search));Object.defineProperties(ClassProto,{href:{get:function(){return href.get.call(this)},set:function(value){var sp=this._searchParams;href.set.call(this,value);if(sp)updateSearchParams(sp)}},search:{get:function(){return search.get.call(this)},set:function(value){var sp=this._searchParams;search.set.call(this,value);if(sp)updateSearchParams(sp)}},searchParams:{get:function(){verifySearchParams(this,Class);return this._searchParams||createSearchParams(this,new URLSearchParams(this.search.slice(1)))},set:function(sp){verifySearchParams(this,Class);createSearchParams(this,sp)}}})}};upgradeClass(HTMLAnchorElement);if(/^function|object$/.test(typeof URL)&&URL.prototype)upgradeClass(URL);return URLSearchParams}();(function(URLSearchParamsProto){var iterable=function(){try{return!!Symbol.iterator}catch(error){return false}}();if(!("forEach"in URLSearchParamsProto)){URLSearchParamsProto.forEach=function forEach(callback,thisArg){var names=Object.create(null);this.toString().replace(/=[\s\S]*?(?:&|$)/g,"=").split("=").forEach(function(name){if(!name.length||name in names)return;(names[name]=this.getAll(name)).forEach(function(value){callback.call(thisArg,value,name,this)},this)},this)}}if(!("keys"in URLSearchParamsProto)){URLSearchParamsProto.keys=function keys(){var items=[];this.forEach(function(value,name){items.push(name)});var iterator={next:function(){var value=items.shift();return{done:value===undefined,value:value}}};if(iterable){iterator[Symbol.iterator]=function(){return iterator}}return iterator}}if(!("values"in URLSearchParamsProto)){URLSearchParamsProto.values=function values(){var items=[];this.forEach(function(value){items.push(value)});var iterator={next:function(){var value=items.shift();return{done:value===undefined,value:value}}};if(iterable){iterator[Symbol.iterator]=function(){return iterator}}return iterator}}if(!("entries"in URLSearchParamsProto)){URLSearchParamsProto.entries=function entries(){var items=[];this.forEach(function(value,name){items.push([name,value])});var iterator={next:function(){var value=items.shift();return{done:value===undefined,value:value}}};if(iterable){iterator[Symbol.iterator]=function(){return iterator}}return iterator}}if(iterable&&!(Symbol.iterator in URLSearchParamsProto)){URLSearchParamsProto[Symbol.iterator]=URLSearchParamsProto.entries}if(!("sort"in URLSearchParamsProto)){URLSearchParamsProto.sort=function sort(){var entries=this.entries(),entry=entries.next(),done=entry.done,keys=[],values=Object.create(null),i,key,value;while(!done){value=entry.value;key=value[0];keys.push(key);if(!(key in values)){values[key]=[]}values[key].push(value[1]);entry=entries.next();done=entry.done}keys.sort();for(i=0;i<keys.length;i++){this["delete"](keys[i])}for(i=0;i<keys.length;i++){key=keys[i];this.append(key,values[key].shift())}}}})(URLSearchParams.prototype);
|