servel 0.19.0 → 0.20.0
Sign up to get free protection for your applications and to get access to all the features.
- 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);
|