Compare commits
978 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efa77bf534 | |||
| bdb4e617e6 | |||
| e159e20b7c | |||
| 1bf1c56fd8 | |||
| ffc2b28a09 | |||
| 12aab494ad | |||
| 2b2a1ba8bd | |||
| 4bd104a742 | |||
| c70d9f35c1 | |||
| 95bd859236 | |||
| cb89522936 | |||
| 6146308106 | |||
| 6fd4c48cc7 | |||
| 589aa591ee | |||
| 990925e85e | |||
| ab8221889a | |||
| 9e5d65651c | |||
| edbc159fad | |||
|
|
e278b9df6a | ||
|
|
daef56f970 | ||
|
|
e3ca756aa7 | ||
|
|
6208eb0840 | ||
|
|
0830af4f56 | ||
|
|
236b9324f5 | ||
|
|
603354cc4b | ||
|
|
7effdfeccd | ||
|
|
bfcc60323e | ||
|
|
7b1d361d69 | ||
|
|
a121a9b7c3 | ||
|
|
6306a88e55 | ||
|
|
9d8b915905 | ||
|
|
0b51c9825d | ||
|
|
b829fc026a | ||
|
|
0dcebf88af | ||
|
|
1ada29c5e9 | ||
|
|
9d85aea268 | ||
|
|
98dc34358b | ||
|
|
753053e586 | ||
|
|
c9a35ac933 | ||
|
|
6290b79886 | ||
|
|
1f7bd30ddf | ||
|
|
ccf29b1117 | ||
|
|
cd6f360628 | ||
|
|
a57ce2d840 | ||
|
|
aa36a17ef9 | ||
|
|
83be75967c | ||
|
|
e5f88d4002 | ||
|
|
ee3720c59e | ||
|
|
b1d11308d7 | ||
|
|
0c5c3e7dd5 | ||
|
|
3851c4ef3e | ||
|
|
d27adbd77a | ||
|
|
1ef39dc0fb | ||
|
|
f1a2e290ae | ||
|
|
3b1e78ae02 | ||
|
|
21162495cb | ||
|
|
bc85e0c33f | ||
|
|
f6ade2ed53 | ||
|
|
3f3f3e4887 | ||
|
|
16d856c7df | ||
|
|
d862268a32 | ||
|
|
db03ef0018 | ||
|
|
c00d967159 | ||
|
|
d38c72e4d6 | ||
|
|
2680ee600b | ||
|
|
6f3ed2cd80 | ||
|
|
e36752c4f9 | ||
|
|
db321bf76e | ||
|
|
89e676424a | ||
|
|
1658fe3bb9 | ||
|
|
7ed5d9005c | ||
|
|
0c27de5fc1 | ||
|
|
20dd5e0ae2 | ||
|
|
442f02ff37 | ||
|
|
56c421ed63 | ||
|
|
18a199d609 | ||
|
|
1fd0d99342 | ||
|
|
2d18dbf23b | ||
|
|
cf9dffb05c | ||
|
|
3a054fb055 | ||
|
|
7c3bbc5323 | ||
|
|
9d88e2dbf4 | ||
|
|
aebb509965 | ||
|
|
616bab254a | ||
|
|
47ec669a38 | ||
|
|
812a9ab0f4 | ||
|
|
e3235dc802 | ||
|
|
d25563f94e | ||
|
|
4eb3f0c8b1 | ||
|
|
1af0f098d1 | ||
|
|
320b9242d5 | ||
|
|
258abb3bf5 | ||
|
|
84fdc64070 | ||
|
|
9266906d15 | ||
|
|
d760a537c9 | ||
|
|
dc51487d50 | ||
|
|
63de70611d | ||
|
|
46fad2bef2 | ||
|
|
e07dbf81d2 | ||
|
|
22fb006d7a | ||
|
|
05f335b105 | ||
|
|
e801485ec1 | ||
|
|
28c28191a2 | ||
|
|
c280160e8b | ||
|
|
03fbb34d95 | ||
|
|
4712047015 | ||
|
|
010d57bf4c | ||
|
|
c81706061b | ||
|
|
746b4234d2 | ||
|
|
97b7943fb1 | ||
|
|
54335422cf | ||
|
|
006ad75070 | ||
|
|
1d9b723053 | ||
|
|
b14359b421 | ||
|
|
4b228cccf0 | ||
|
|
0bb72c03fc | ||
|
|
f9ebb9a053 | ||
|
|
95aa547196 | ||
|
|
90acace1a7 | ||
|
|
23079fedd9 | ||
|
|
7028260458 | ||
|
|
d6de71b1e8 | ||
|
|
85b42274e4 | ||
|
|
608ee01f55 | ||
|
|
e040cecf68 | ||
|
|
377baf1edd | ||
|
|
b6d5445b31 | ||
|
|
68519d8407 | ||
|
|
a37bb58ef3 | ||
|
|
38ad5a85a4 | ||
|
|
a735c0f4e8 | ||
|
|
79a31b84c6 | ||
|
|
5e04493329 | ||
|
|
da1ffeb47d | ||
|
|
ed8177e05f | ||
|
|
2362673c6f | ||
|
|
cdc25a2c1e | ||
|
|
f1f444de39 | ||
|
|
ea8f3b1334 | ||
|
|
4453115030 | ||
|
|
62ecdaac3b | ||
|
|
842a722df8 | ||
|
|
d7385c5d06 | ||
|
|
fa934d8c9b | ||
|
|
4810e10343 | ||
|
|
4a9fb8e505 | ||
|
|
1c0cce52b5 | ||
|
|
77557d1699 | ||
|
|
f582612619 | ||
|
|
4bbc5ea972 | ||
|
|
5b5f374255 | ||
|
|
11df35209a | ||
|
|
37105f6aa6 | ||
|
|
e1413972bf | ||
|
|
4dbf6e08fc | ||
|
|
6113327075 | ||
|
|
0ff94bc4f5 | ||
|
|
cf1fad0509 | ||
|
|
48e021a532 | ||
|
|
55ea9e5963 | ||
|
|
671e402db4 | ||
|
|
c8064a9eb1 | ||
|
|
69f8872755 | ||
|
|
f6c0965f85 | ||
|
|
bdb4f1d7bd | ||
|
|
70efdf877e | ||
|
|
69d477eeb1 | ||
|
|
9ec77ede6f | ||
|
|
e2e38c0db9 | ||
|
|
ea71566121 | ||
|
|
e919f76ea4 | ||
|
|
7a5234dbce | ||
|
|
fc9818ae4b | ||
|
|
da8cfa9222 | ||
|
|
7ec157cf62 | ||
|
|
9e54506dcc | ||
|
|
6b6fc91b6a | ||
|
|
0a422f748d | ||
|
|
4b09c6c26b | ||
|
|
f8336b2220 | ||
|
|
0dc3b6330b | ||
|
|
6633e3f5b1 | ||
|
|
3f1c5398a0 | ||
|
|
f548f37a59 | ||
|
|
3fbd4739ff | ||
|
|
ae4fddbfaa | ||
|
|
6333caa0b5 | ||
|
|
4fc7e1baec | ||
|
|
c1d95bf569 | ||
|
|
e9c4215f88 | ||
|
|
44d99e2be5 | ||
|
|
6580bd73ed | ||
|
|
377d99c0f1 | ||
|
|
a92188ae1f | ||
|
|
9b86106278 | ||
|
|
6892a0ded5 | ||
|
|
e3d954287d | ||
|
|
3715776f6d | ||
|
|
62fe7ee1fe | ||
|
|
b1483bbf45 | ||
|
|
f717c6a82f | ||
|
|
d88db002f1 | ||
|
|
5fba1a4300 | ||
|
|
1e5e9f0209 | ||
|
|
443650b217 | ||
|
|
fbb194e44c | ||
|
|
c7fc29ba99 | ||
|
|
46a05cde84 | ||
|
|
48e2f5bdfb | ||
|
|
d869589b47 | ||
|
|
ff051beae7 | ||
|
|
aef16fe812 | ||
|
|
8a41d2d278 | ||
|
|
6e7ca0a1dc | ||
|
|
e3c4726cda | ||
|
|
c742f86924 | ||
|
|
5b19668b38 | ||
|
|
80d6b6ef92 | ||
|
|
8cc56243e6 | ||
|
|
6c199dbe4a | ||
|
|
790b9d422f | ||
|
|
02c54566a9 | ||
|
|
de8478ddea | ||
|
|
b89ae3b7d1 | ||
|
|
cd6b8a72ca | ||
|
|
d0661dd697 | ||
|
|
d0ab0f6566 | ||
|
|
c5475478ad | ||
|
|
ecd075c369 | ||
|
|
eae5611b07 | ||
|
|
564b064f50 | ||
|
|
9e20bb65bd | ||
|
|
bb236ff1be | ||
|
|
4d3c5fee3e | ||
|
|
6d12f4124c | ||
|
|
143f6b5b44 | ||
|
|
b305a60c8d | ||
|
|
ed1939c7ac | ||
|
|
5f4dbbd67b | ||
|
|
20dcb3659a | ||
|
|
bd85d39af5 | ||
|
|
ea66a89ce1 | ||
|
|
263eccc363 | ||
|
|
49f6666c13 | ||
|
|
5d02a28468 | ||
|
|
b4d682da69 | ||
|
|
53c377f274 | ||
|
|
629ac4654d | ||
|
|
53f6b46162 | ||
|
|
fc4273b585 | ||
|
|
4e45475ea9 | ||
|
|
e81508cc48 | ||
|
|
9692899c9b | ||
|
|
88e6fd0e63 | ||
|
|
eb84dc13dd | ||
|
|
f6e27ba9bf | ||
|
|
edc95de507 | ||
|
|
78f460f391 | ||
|
|
7810374fa4 | ||
|
|
4f775afa98 | ||
|
|
e939b28ca5 | ||
|
|
5795622430 | ||
|
|
78aacdee51 | ||
|
|
bd2d07c8eb | ||
|
|
3b04f4a47d | ||
|
|
6d8c2df915 | ||
|
|
52c64d13c3 | ||
|
|
c2cf15d797 | ||
|
|
605d3ce84f | ||
|
|
3ca566dfee | ||
|
|
4da9cf2e86 | ||
|
|
3c31ccf6fe | ||
|
|
9869d3c084 | ||
|
|
35cb49098b | ||
|
|
229e08fd36 | ||
|
|
47dfd430d2 | ||
|
|
eeb3a14ed3 | ||
|
|
f1e8f6ec69 | ||
|
|
0a39ef2fe6 | ||
|
|
26a407439b | ||
|
|
f7c20a0951 | ||
|
|
3c94553ae2 | ||
|
|
ae88a23a3c | ||
|
|
1073b96748 | ||
|
|
0e489074c7 | ||
|
|
d6f7e39075 | ||
|
|
32ee7378e9 | ||
|
|
219a111120 | ||
|
|
06c186b970 | ||
|
|
04a74e2eec | ||
|
|
f52b378611 | ||
|
|
7a5b17f1f2 | ||
|
|
69620d806d | ||
|
|
fb8e4bf687 | ||
|
|
e2a3eeaf43 | ||
|
|
b6a40e7e7c | ||
|
|
1e22c7f2dc | ||
|
|
e8c607b1ed | ||
|
|
b971c97673 | ||
|
|
9b391152af | ||
|
|
6f10b87e9f | ||
|
|
21c7dd841d | ||
|
|
240ebc3d88 | ||
|
|
7c7838967d | ||
|
|
079d0a0d2f | ||
|
|
e3ff140621 | ||
|
|
bd2f4b049d | ||
|
|
d593c2d646 | ||
|
|
3a236c3af3 | ||
|
|
dafbfc3c9c | ||
|
|
60324848e6 | ||
|
|
197e393869 | ||
|
|
4cbef95b5a | ||
|
|
7435dbc4dc | ||
|
|
17f5258d82 | ||
|
|
fc5279aee9 | ||
|
|
c082338d72 | ||
|
|
c750b22bb6 | ||
|
|
dcd8c2002e | ||
|
|
48dee68014 | ||
|
|
f8b6da0d4c | ||
|
|
4383dc435a | ||
|
|
46f06a6927 | ||
|
|
ca32c6e491 | ||
|
|
b7231a35a5 | ||
|
|
321dd1929f | ||
|
|
046666b524 | ||
|
|
03c39ea378 | ||
|
|
be3eea61f3 | ||
|
|
2c0a87ddb3 | ||
|
|
709c1eb8c4 | ||
|
|
27e16052e7 | ||
|
|
d358f934a7 | ||
|
|
cf6372f45d | ||
|
|
c792f7ebd1 | ||
|
|
6bae530e53 | ||
|
|
6417f4ab0b | ||
|
|
f7bf489601 | ||
|
|
4484791794 | ||
|
|
c96c2ff97b | ||
|
|
2031f42229 | ||
|
|
8ae599fade | ||
|
|
5c0fdf0029 | ||
|
|
a52744ab9b | ||
|
|
34ba9cd812 | ||
|
|
43a75c02f9 | ||
|
|
99d2518d92 | ||
|
|
d44fb2ce81 | ||
|
|
7c18b84ff4 | ||
|
|
77b55f6193 | ||
|
|
61cfc5e44f | ||
|
|
199111e48a | ||
|
|
7db45caf20 | ||
|
|
d2d8ccd720 | ||
|
|
91f33002e3 | ||
|
|
027bc9d583 | ||
|
|
e22d785b1e | ||
|
|
31b571db52 | ||
|
|
9838d6af39 | ||
|
|
a8df9a5f5a | ||
|
|
5e09bd3818 | ||
|
|
6be09e35f8 | ||
|
|
0c03f71412 | ||
|
|
e13ac03e0c | ||
|
|
5ff6eab1f7 | ||
|
|
06466fa374 | ||
|
|
711e323d0d | ||
|
|
f024942b21 | ||
|
|
b807b8f2d2 | ||
|
|
f73ba58354 | ||
|
|
44accc4d2b | ||
|
|
32b9711ec9 | ||
|
|
7f43db089c | ||
|
|
b316108788 | ||
|
|
a05b52ce74 | ||
|
|
79fe4e3a9a | ||
|
|
49418e56cf | ||
|
|
9637865984 | ||
|
|
f9cf7d9435 | ||
|
|
9a62eb202b | ||
|
|
cfae79d0f6 | ||
|
|
415709ba91 | ||
|
|
100065bd7e | ||
|
|
ff0be0275f | ||
|
|
623b5b47f8 | ||
|
|
c1763d88c8 | ||
|
|
50d300dd7a | ||
|
|
a492bdde15 | ||
|
|
88fce4c4a0 | ||
|
|
47dedbc25d | ||
|
|
dd3736ccd5 | ||
|
|
74f767aaba | ||
|
|
6c024ae354 | ||
|
|
e4dce0d5ad | ||
|
|
d9f549e481 | ||
|
|
1c8598423d | ||
|
|
b4e25e72f2 | ||
|
|
348ecdd253 | ||
|
|
3f9249b8bc | ||
|
|
b85cb62588 | ||
|
|
c3057cab0a | ||
|
|
0c3e723c16 | ||
|
|
2534a9b931 | ||
|
|
6fbb96e54e | ||
|
|
a2a373c43f | ||
|
|
7e9235503f | ||
|
|
5337eb81af | ||
|
|
eb56953815 | ||
|
|
462185f7b1 | ||
|
|
0a6c69bd57 | ||
|
|
e9ca3566b8 | ||
|
|
7818ac870b | ||
|
|
d5a8537216 | ||
|
|
309a8d8365 | ||
|
|
e5d2dc998e | ||
|
|
a0a02922dd | ||
|
|
334f53d91a | ||
|
|
faa0b24a17 | ||
|
|
e20d60ee8e | ||
|
|
57a5e0a816 | ||
|
|
9d35550d5f | ||
|
|
94c4ad1bb2 | ||
|
|
f9cec4fedc | ||
|
|
e585353c00 | ||
|
|
a709160a40 | ||
|
|
a38e977e0d | ||
|
|
103f986fcd | ||
|
|
38a82f9d2d | ||
|
|
7af7268e7d | ||
|
|
ed1bdaa52b | ||
|
|
f28bc91133 | ||
|
|
bb3a1688bf | ||
|
|
e2d4389757 | ||
|
|
48186e883e | ||
|
|
d83e4e8732 | ||
|
|
24f8bf459e | ||
|
|
82cc513b4a | ||
|
|
86aae8ee34 | ||
|
|
e4690f9f96 | ||
|
|
41efcc7233 | ||
|
|
81dab72efb | ||
|
|
d9a9477ee2 | ||
|
|
cc01e95539 | ||
|
|
042ca3abfb | ||
|
|
1099445c30 | ||
|
|
8c707c9c8b | ||
|
|
5bae39b4c0 | ||
|
|
da57930dac | ||
|
|
e3c348de36 | ||
|
|
491e5f894e | ||
|
|
d20b1359da | ||
|
|
6dc1173164 | ||
|
|
caea1a3adb | ||
|
|
bfe7c61a99 | ||
|
|
ec0e34c882 | ||
|
|
07f62abccb | ||
|
|
2f0f875c5a | ||
|
|
48728e3762 | ||
|
|
5c4fffa0e7 | ||
|
|
1b3f890441 | ||
|
|
7df05960b1 | ||
|
|
7a99fd6b0b | ||
|
|
90b00e8f24 | ||
|
|
6537ce19d7 | ||
|
|
d5761b9afd | ||
|
|
630b0fb6c2 | ||
|
|
d1dbece28c | ||
|
|
b2c772c580 | ||
|
|
066b3814d3 | ||
|
|
20a3f101a3 | ||
|
|
127eb6dce9 | ||
|
|
f8c8d40614 | ||
|
|
3be54a813a | ||
|
|
8741ec12a9 | ||
|
|
f62f976e58 | ||
|
|
764d6e6a49 | ||
|
|
9cd71e5eda | ||
|
|
650e72e94c | ||
|
|
6413ab668a | ||
|
|
5e26facaef | ||
|
|
9387f21b74 | ||
|
|
3132a29197 | ||
|
|
e5803d2b04 | ||
|
|
6923a837fa | ||
|
|
e64c899cfd | ||
|
|
86548dd4aa | ||
|
|
a0f044d52e | ||
|
|
fb8c28b508 | ||
|
|
4e8477d098 | ||
|
|
5082ad7b99 | ||
|
|
442854886e | ||
|
|
e1a5513c58 | ||
|
|
ea577c46ca | ||
|
|
22598a79eb | ||
|
|
27c229b676 | ||
|
|
21b15f283d | ||
|
|
9573db6aa1 | ||
|
|
e2fdc9adaa | ||
|
|
47e09cf412 | ||
|
|
1e305f01b7 | ||
|
|
e6a8d3cb34 | ||
|
|
f3dc3805c9 | ||
|
|
29e81d44bd | ||
|
|
c652e63b6d | ||
|
|
148a01079b | ||
|
|
2855ba7133 | ||
|
|
e58fc32fc6 | ||
|
|
415cbf29b7 | ||
|
|
27b1fa62ca | ||
|
|
deba8f3ba2 | ||
|
|
205be6a330 | ||
|
|
aea19e5a83 | ||
|
|
3aaa623804 | ||
|
|
0b31a69064 | ||
|
|
99fb8e010d | ||
|
|
da57c38ddd | ||
|
|
f357b60ceb | ||
|
|
bc856ddec6 | ||
|
|
173319aa8c | ||
|
|
ffe583b52c | ||
|
|
022b094bda | ||
|
|
6043547972 | ||
|
|
5245010167 | ||
|
|
1ff23b8bab | ||
|
|
b673c92461 | ||
|
|
8dca0494b6 | ||
|
|
5e363fd4c8 | ||
|
|
4f63213dfc | ||
|
|
c137d938de | ||
|
|
bd4f97335d | ||
|
|
a142def001 | ||
|
|
d9966a0fe7 | ||
|
|
2f8b4932ed | ||
|
|
af1e3009a6 | ||
|
|
c4ebf4d308 | ||
|
|
918ceb4852 | ||
|
|
27721c795e | ||
|
|
f42e0ea069 | ||
|
|
91c4ed14fe | ||
|
|
f59fd90293 | ||
|
|
64d7de6b7a | ||
|
|
85e56f7586 | ||
|
|
e4bb6b99bd | ||
|
|
107537d5dd | ||
|
|
0d0fd61bb6 | ||
|
|
001ce36336 | ||
|
|
3aff1b4c1f | ||
|
|
d9e7c47899 | ||
|
|
3ed0d5cc90 | ||
|
|
5e587a7664 | ||
|
|
25ee93d5eb | ||
|
|
564bec6796 | ||
|
|
4a1bf3e7a1 | ||
|
|
fc3e1b1da0 | ||
|
|
eff3b4e49d | ||
|
|
ee017e064f | ||
|
|
9eac2f92e5 | ||
|
|
920c8f96e0 | ||
|
|
ba4fd4844e | ||
|
|
ceae98b4f9 | ||
|
|
b9519ff074 | ||
|
|
6c7a5c6e06 | ||
|
|
4a53178cba | ||
|
|
506160deda | ||
|
|
66c06d91c3 | ||
|
|
ca9d46d909 | ||
|
|
a6a27a81fd | ||
|
|
8d7efcc06a | ||
|
|
45bc1709f5 | ||
|
|
6737281be8 | ||
|
|
02569b83e6 | ||
|
|
e3592d1149 | ||
|
|
7eabf070a8 | ||
|
|
593b40aaee | ||
|
|
0a8384c628 | ||
|
|
5f8bfc558c | ||
|
|
26339db85b | ||
|
|
fd2ef30b4a | ||
|
|
3982cd377a | ||
|
|
2675d79c92 | ||
|
|
d1bc021255 | ||
|
|
c2dd492aa1 | ||
|
|
588e3c145a | ||
|
|
acb1dc900a | ||
|
|
d28dd31460 | ||
|
|
65e7944d4d | ||
|
|
15b3ea4a23 | ||
|
|
08139a3845 | ||
|
|
f768e38d18 | ||
|
|
a165447c3c | ||
|
|
602627e17a | ||
|
|
83580033d3 | ||
|
|
3115897c92 | ||
|
|
2d955137d7 | ||
|
|
ff730e2271 | ||
|
|
ff03cc6c28 | ||
|
|
ec265ac11c | ||
|
|
c939c556ea | ||
|
|
13825c7a5f | ||
|
|
49169806ff | ||
|
|
17e1f99b65 | ||
|
|
f0e1d22791 | ||
|
|
8c2339cdde | ||
|
|
824d1e78d9 | ||
|
|
1f4c9d2ddb | ||
|
|
4b9a21a5a6 | ||
|
|
82c6b75194 | ||
|
|
80691061fc | ||
|
|
55164953df | ||
|
|
52e1ddd014 | ||
|
|
7648da2334 | ||
|
|
22bce37a9a | ||
|
|
e9dc9f0e0d | ||
|
|
9c9d2d72f0 | ||
|
|
eb70262ad6 | ||
|
|
f1dd369fa7 | ||
|
|
32c084ea0a | ||
|
|
af8ec817bc | ||
|
|
db264d9f14 | ||
|
|
9498ae5f00 | ||
|
|
8b86540b80 | ||
|
|
bd54366d5c | ||
|
|
9c59c3ebfa | ||
|
|
bc05aeaa80 | ||
|
|
a8bb356613 | ||
|
|
d80f90d9d7 | ||
|
|
87a5620ccb | ||
|
|
9f54982f37 | ||
|
|
15aabd341f | ||
|
|
ff1ab54376 | ||
|
|
e88e73cd68 | ||
|
|
515d56509c | ||
|
|
1b37d5f074 | ||
|
|
16f9e09e86 | ||
|
|
bb432c9694 | ||
|
|
bf0eb42fca | ||
|
|
3b2889d20b | ||
|
|
54d01d03da | ||
|
|
b020ae84a7 | ||
|
|
0a85fce882 | ||
|
|
6d48ea904b | ||
|
|
510485a9b2 | ||
|
|
024ed51b1a | ||
|
|
e8718ce104 | ||
|
|
b164588b48 | ||
|
|
697893e8c9 | ||
|
|
6e1b4d97ff | ||
|
|
f8a9c5c360 | ||
|
|
5653a8f6a0 | ||
|
|
28aa40ee2b | ||
|
|
70a9237951 | ||
|
|
becb98cfc5 | ||
|
|
ed432c52fb | ||
|
|
0a746632a3 | ||
|
|
08d58048d2 | ||
|
|
a304ba0fb2 | ||
|
|
790914075d | ||
|
|
80bd703372 | ||
|
|
28c74f33c1 | ||
|
|
f82a2b6062 | ||
|
|
150382aa5a | ||
|
|
fd8df06cb5 | ||
|
|
6dac88a0e3 | ||
|
|
9a0c7a8501 | ||
|
|
6d2ca36511 | ||
|
|
3616d74c0a | ||
|
|
f7af72f169 | ||
|
|
9f7bdacf5a | ||
|
|
34e58cad37 | ||
|
|
5a2cf1c60c | ||
|
|
f3ca0d1f46 | ||
|
|
f99afcbbf1 | ||
|
|
bb2069f68d | ||
|
|
d1e5428b42 | ||
|
|
2c4130cc64 | ||
|
|
0ba7fa0593 | ||
|
|
d062fbc0a7 | ||
|
|
cec052d755 | ||
|
|
d862cae98e | ||
|
|
9d5d10712d | ||
|
|
9a7f4315c2 | ||
|
|
024f5f1d08 | ||
|
|
3a6f221840 | ||
|
|
acd15893d1 | ||
|
|
e32ebec736 | ||
|
|
7f5d2843c1 | ||
|
|
7b44dc4fbf | ||
|
|
0f7aa54c6c | ||
|
|
e22046882b | ||
|
|
e261f4c4c9 | ||
|
|
74fceed06d | ||
|
|
b2789c3195 | ||
|
|
fd5a9537d4 | ||
|
|
c32209c1f0 | ||
|
|
ce7e44c551 | ||
|
|
23718d229c | ||
|
|
beb3ab5cd1 | ||
|
|
fd5d0212e9 | ||
|
|
5f21b127a8 | ||
|
|
df2e9b2c85 | ||
|
|
0fbbf2e389 | ||
|
|
f47532c299 | ||
|
|
8cd4837993 | ||
|
|
e9356c4424 | ||
|
|
8a9fe8eed5 | ||
|
|
13d25945a6 | ||
|
|
3583b74262 | ||
|
|
e3fcdd5218 | ||
|
|
832788b287 | ||
|
|
9f93d8a9c1 | ||
|
|
7978432a11 | ||
|
|
8c09ef5f62 | ||
|
|
094628f55c | ||
|
|
af17d924ae | ||
|
|
a41a055579 | ||
|
|
62b4f6cc38 | ||
|
|
1deb42e91f | ||
|
|
f4b483581a | ||
|
|
1a481d20c7 | ||
|
|
f5af0e34d9 | ||
|
|
389b6ad717 | ||
|
|
c934609fce | ||
|
|
4420c2b1b3 | ||
|
|
a7c4cc2fd1 | ||
|
|
fe6e96f8f2 | ||
|
|
904d485839 | ||
|
|
1543ccd88a | ||
|
|
687750fd18 | ||
|
|
6a26639453 | ||
|
|
3cacc9fb00 | ||
|
|
28cfae03a8 | ||
|
|
7ee148ea62 | ||
|
|
76e4c18e80 | ||
|
|
1203aae5b4 | ||
|
|
c127653d93 | ||
|
|
64a3d5083a | ||
|
|
2dad313353 | ||
|
|
67673ddc71 | ||
|
|
3b25e2e41b | ||
|
|
c44cd06bb5 | ||
|
|
b8a7caef10 | ||
|
|
41f49860b5 | ||
|
|
84a432dc9f | ||
|
|
dee0745745 | ||
|
|
313f865de1 | ||
|
|
b9d0bcd3d3 | ||
|
|
dd95313776 | ||
|
|
0e739d4b48 | ||
|
|
0770452f78 | ||
|
|
d56074df36 | ||
|
|
ed14e94923 | ||
|
|
33661acb4b | ||
|
|
42045eaa0a | ||
|
|
3343b6ea9c | ||
|
|
c7d5369cb1 | ||
|
|
97bb40280d | ||
|
|
04a37961ae | ||
|
|
a6e789009b | ||
|
|
29c92ce408 | ||
|
|
4f9d644138 | ||
|
|
d8fa0a6287 | ||
|
|
45e96b7070 | ||
|
|
8c7753024d | ||
|
|
fc12fdc610 | ||
|
|
1474977e37 | ||
|
|
3b853775b6 | ||
|
|
e85d6ecf3b | ||
|
|
a1424ef5ff | ||
|
|
090173d00e | ||
|
|
1a146f10c6 | ||
|
|
d1d89caccf | ||
|
|
d01f2e4a88 | ||
|
|
89a64eda09 | ||
|
|
a0ce7db4e8 | ||
|
|
02e279fdc3 | ||
|
|
efbbdf4f7e | ||
|
|
dc417fedfb | ||
|
|
2eee6bc620 | ||
|
|
abd897094d | ||
|
|
8091b6a338 | ||
|
|
73fcb2d5bf | ||
|
|
60ff1d8870 | ||
|
|
bc3188e84e | ||
|
|
dfb4ad0c14 | ||
|
|
6c4a2657ec | ||
|
|
750d7a1206 | ||
|
|
e74e3b090e | ||
|
|
9825e485a5 | ||
|
|
5a74dcaf87 | ||
|
|
816e46ceb6 | ||
|
|
38376f5646 | ||
|
|
46bd62f5fc | ||
|
|
e642592db8 | ||
|
|
fe4719c8d3 | ||
|
|
f25d71b580 | ||
|
|
6df48b9263 | ||
|
|
64caa87235 | ||
|
|
fbf0b19455 | ||
|
|
87b70bc260 | ||
|
|
9a110356db | ||
|
|
887eadb372 | ||
|
|
024659d004 | ||
|
|
3b0a099369 | ||
|
|
89445ddd14 | ||
|
|
f31010756c | ||
|
|
ea673ccb5f | ||
|
|
0656b5a12a | ||
|
|
19c0a37ade | ||
|
|
5989f52f96 | ||
|
|
2a9d9809d9 | ||
|
|
5c7c7419a4 | ||
|
|
a1b20cc12f | ||
|
|
4a5644a927 | ||
|
|
61a3160575 | ||
|
|
f80c8d1d66 | ||
|
|
cd26790ae5 | ||
|
|
94349b1dee | ||
|
|
dbbb22e43a | ||
|
|
8425bdde1f | ||
|
|
7f11098a48 | ||
|
|
2f8a7d1320 | ||
|
|
a40fab6b42 | ||
|
|
0aebfa4e60 | ||
|
|
1ce9b925fe | ||
|
|
cb51fffea8 | ||
|
|
67d0d18b9d | ||
|
|
b4e5aa1fe6 | ||
|
|
85cf2de84f | ||
|
|
bdbab771f7 | ||
|
|
3d64e51638 | ||
|
|
8a42c06bad | ||
|
|
7b966b80c3 | ||
|
|
46a820b57a | ||
|
|
0805958e36 | ||
|
|
07c975ccd6 | ||
|
|
459b550258 | ||
|
|
f20edf1aa0 | ||
|
|
8e70ccb770 | ||
|
|
7664907229 | ||
|
|
d88d8e9f3b | ||
|
|
0d695a4bf8 | ||
|
|
f0ee618d87 | ||
|
|
a0c98e1faf | ||
|
|
3785926257 | ||
|
|
69ca455bdb | ||
|
|
c962304565 | ||
|
|
3b42543f08 | ||
|
|
72ce778e18 | ||
|
|
71ae6cf960 | ||
|
|
a96ac37288 | ||
|
|
35297a13d3 | ||
|
|
a82aa8144d | ||
|
|
e431727937 | ||
|
|
6d2307efdf | ||
|
|
89c1811d83 | ||
|
|
7ed31eb9ab | ||
|
|
92cdae1baf | ||
|
|
eed49bb6d4 | ||
|
|
3ebe5a36d0 | ||
|
|
80563bd542 | ||
|
|
d81ddbd9e9 | ||
|
|
1474e92040 | ||
|
|
9e711287fe | ||
|
|
3b5dcfc59b | ||
|
|
58b3f46adc | ||
|
|
f5f0ffbdc8 | ||
|
|
0f93c7bc60 | ||
|
|
f6587f8029 | ||
|
|
eca7ecefcc | ||
|
|
9ab8fe4ed0 | ||
|
|
ffa1fdd515 | ||
|
|
b50de077ba | ||
|
|
5a1786c958 | ||
|
|
cb53075bb9 | ||
|
|
83c5178cc8 | ||
|
|
ca987490b0 | ||
|
|
149ec0f4d7 | ||
|
|
283065ee76 | ||
|
|
a710cf5055 | ||
|
|
f7a6b624f3 | ||
|
|
c3f4ef4fc2 | ||
|
|
b8b84c55ef | ||
|
|
42afbbd805 | ||
|
|
e96cad5dad | ||
|
|
4acb72aea5 | ||
|
|
dba1b78ee9 | ||
|
|
34f72f2772 | ||
|
|
848ffe53d5 | ||
|
|
5cda6ef8c7 | ||
|
|
b0458208db | ||
|
|
bad418cebf | ||
|
|
68cc338736 | ||
|
|
9e960ed19b | ||
|
|
ad56f72500 | ||
|
|
485ffe9c78 | ||
|
|
b647d828f9 | ||
|
|
0e851f22a0 | ||
|
|
9d83bf648d | ||
|
|
dfedb6ef45 | ||
|
|
9877f33771 | ||
|
|
255ab82c4d | ||
|
|
97b506cc12 | ||
|
|
7e2c11dd03 | ||
|
|
dc52c816eb | ||
|
|
2718b7bfc9 | ||
|
|
638093d78e | ||
|
|
cb4e514425 | ||
|
|
2754d82c9e | ||
|
|
c0c5c91c5c | ||
|
|
65f086c70a | ||
|
|
cbda444af7 | ||
|
|
c4c4684068 | ||
|
|
220cde48c9 | ||
|
|
d8fe5ec64e | ||
|
|
54ba5b1eec | ||
|
|
f4f35c1fc9 | ||
|
|
f00f22d3b2 | ||
|
|
d31c8fe98b | ||
|
|
50846b6592 | ||
|
|
7648518c28 | ||
|
|
637b6197cb | ||
|
|
abf2c52e35 | ||
|
|
e267605bab | ||
|
|
465468bad4 | ||
|
|
0a9f817a30 | ||
|
|
b48d790c0d | ||
|
|
1d71209325 | ||
|
|
b38abe28a2 | ||
|
|
36eeb578ee | ||
|
|
e8a9590631 | ||
|
|
eae9beb46a | ||
|
|
eb3cadf75a | ||
|
|
b4e3e71a30 | ||
|
|
e682c44c1a | ||
|
|
638079f511 | ||
|
|
8bcc1646b8 | ||
|
|
20395c16be | ||
|
|
54f54ad773 | ||
|
|
62995c7bc3 | ||
|
|
eec10c81dc | ||
|
|
e51061c984 | ||
|
|
792702b14f | ||
|
|
34320aa35c | ||
|
|
f7274737ae | ||
|
|
ab77bbdf55 | ||
|
|
b75be8eb52 | ||
|
|
33309dd70a | ||
|
|
1a3953f242 | ||
|
|
2cf67a6c1c | ||
|
|
973fc653be | ||
|
|
4a939ace77 | ||
|
|
0a8b4da8ca | ||
|
|
989ef64ca9 | ||
|
|
9b150f5864 | ||
|
|
170dad768a | ||
|
|
680cb07391 | ||
|
|
010366234e | ||
|
|
be2a01b7ad | ||
|
|
6159f7e02f | ||
|
|
c4409967f3 | ||
|
|
2770f46745 | ||
|
|
057b05a693 | ||
|
|
f7c4ab8861 | ||
|
|
44910c5345 | ||
|
|
1970eb94af | ||
|
|
4e792729a6 | ||
|
|
733a3dc4e0 | ||
|
|
f9c2039ed7 | ||
|
|
82625ee5b5 | ||
|
|
c574d4c2fc | ||
|
|
a24ea48680 | ||
|
|
de1544624c | ||
|
|
b5f9f2ca43 | ||
|
|
5cd46f5ae1 | ||
|
|
107055504f | ||
|
|
5f72c9f9d5 | ||
|
|
55f2a0ef2f | ||
|
|
6f23b0d672 |
@ -1,18 +1,27 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
data/uploads/*
|
||||
!data/uploads/.gitkeep
|
||||
data/db/*
|
||||
/data/uploads/*
|
||||
!/data/uploads/.gitkeep
|
||||
/data/db/*
|
||||
!/data/db/.gitkeep
|
||||
/plugins/user/*
|
||||
!/plugins/user/.gitkeep
|
||||
*.lock
|
||||
__pycache__/
|
||||
*.log
|
||||
var/run/*
|
||||
!var/run/.gitkeep
|
||||
/var/run/*
|
||||
!/var/run/.gitkeep
|
||||
*.swp
|
||||
.env
|
||||
venv/
|
||||
node_modules
|
||||
tmp.py
|
||||
!/plugins/user/Dashboard
|
||||
/data/www/plugins/*
|
||||
!/data/www/plugins/.gitkeep
|
||||
/var/run/storage/*
|
||||
!/var/run/storage/.gitkeep
|
||||
11
.env.dist
11
.env.dist
@ -1,4 +1,11 @@
|
||||
# Core
|
||||
DEBUG=false
|
||||
PORT=5000
|
||||
SECRET_KEY=ANY_SECRET_KEY_HERE
|
||||
PLAYER_AUTOSTART_FILE=./var/run/play # Replace by "/dev/null" if not needed
|
||||
|
||||
# Application Server
|
||||
PORT=5000
|
||||
BIND=0.0.0.0
|
||||
EXTERNAL_STORAGE_MOUNTPOINT=%application_dir%/var/run/storage
|
||||
|
||||
# Misc
|
||||
DEMO=false
|
||||
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1,2 +1 @@
|
||||
liberapay: jierka
|
||||
custom: https://paypal.me/jierka
|
||||
|
||||
54
.github/actions/common-docker-build/action.yml
vendored
Executable file
54
.github/actions/common-docker-build/action.yml
vendored
Executable file
@ -0,0 +1,54 @@
|
||||
name: 'Common Docker Build'
|
||||
|
||||
description: 'Reusable action for building and pushing Docker images'
|
||||
|
||||
inputs:
|
||||
build_tags:
|
||||
description: 'Tags for the Docker build'
|
||||
required: true
|
||||
manifest_tags:
|
||||
description: 'Tags for the Docker manifest'
|
||||
required: true
|
||||
flavor:
|
||||
description: 'Flavor for the Docker manifest'
|
||||
required: true
|
||||
docker_username:
|
||||
description: 'DockerHub username'
|
||||
required: true
|
||||
docker_password:
|
||||
description: 'DockerHub password'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ inputs.docker_username }}
|
||||
password: ${{ inputs.docker_password }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ inputs.build_tags }}
|
||||
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7
|
||||
|
||||
- name: Create and push manifest
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: csmith1865/obscreen
|
||||
tags: ${{ inputs.manifest_tags }}
|
||||
flavor: ${{ inputs.flavor }}
|
||||
41
.github/workflows/build-nightly.yml
vendored
Executable file
41
.github/workflows/build-nightly.yml
vendored
Executable file
@ -0,0 +1,41 @@
|
||||
name: Nightly build synced with develop and push docker image
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push-nightly:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: develop
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
|
||||
- name: Sync nightly branch
|
||||
run: |
|
||||
git checkout nightly
|
||||
git merge develop --no-edit
|
||||
git push origin nightly --force
|
||||
|
||||
- name: Checkout nightly branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: nightly
|
||||
|
||||
- name: Call common build workflow
|
||||
uses: ./.github/actions/common-docker-build
|
||||
with:
|
||||
build_tags: csmith1865/obscreen:nightly
|
||||
manifest_tags: type=semver,pattern=nightly
|
||||
flavor: ""
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
28
.github/workflows/build-pr.yml
vendored
Executable file
28
.github/workflows/build-pr.yml
vendored
Executable file
@ -0,0 +1,28 @@
|
||||
name: PR build and push docker image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, labeled]
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
- develop
|
||||
- nightly
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.pull_request.labels.*.name, 'build-pr')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Call common build workflow
|
||||
uses: ./.github/actions/common-docker-build
|
||||
with:
|
||||
build_tags: csmith1865/obscreen:pr-${{ github.event.pull_request.number }}
|
||||
manifest_tags: type=semver,pattern=pr
|
||||
flavor: ""
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
31
.github/workflows/build-release.yml
vendored
Executable file
31
.github/workflows/build-release.yml
vendored
Executable file
@ -0,0 +1,31 @@
|
||||
name: Release build and push docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- 'version.txt'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Read Version
|
||||
id: version
|
||||
run: echo "::set-output name=VERSION::$(cat version.txt)"
|
||||
|
||||
- name: Call common build workflow
|
||||
uses: ./.github/actions/common-docker-build
|
||||
with:
|
||||
build_tags: |
|
||||
csmith1865/obscreen:v${{ steps.version.outputs.VERSION }}
|
||||
csmith1865/obscreen:latest
|
||||
manifest_tags: type=semver,pattern=v${{ steps.version.outputs.VERSION }}
|
||||
flavor: latest=true
|
||||
docker_username: ${{ secrets.DOCKER_USERNAME }}
|
||||
docker_password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
51
.github/workflows/master.yml
vendored
51
.github/workflows/master.yml
vendored
@ -1,51 +0,0 @@
|
||||
name: Build and Push Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'version.txt'
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Read Version
|
||||
id: version
|
||||
run: echo "::set-output name=VERSION::$(cat version.txt)"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
jierka/obscreen:v${{ steps.version.outputs.VERSION }}
|
||||
jierka/obscreen:latest
|
||||
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7
|
||||
|
||||
- name: Create and push manifest
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: jierka/obscreen
|
||||
tags: type=semver,pattern=v{{version}}
|
||||
flavor: |
|
||||
latest=true
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
@ -4,18 +4,27 @@
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
data/uploads/*
|
||||
!data/uploads/.gitkeep
|
||||
data/db/*
|
||||
/data/uploads/*
|
||||
!/data/uploads/.gitkeep
|
||||
/data/db/*
|
||||
!/data/db/.gitkeep
|
||||
/plugins/user/*
|
||||
!/plugins/user/.gitkeep
|
||||
*.lock
|
||||
__pycache__/
|
||||
*.log
|
||||
var/run/*
|
||||
!var/run/.gitkeep
|
||||
/var/run/*
|
||||
!/var/run/.gitkeep
|
||||
*.swp
|
||||
.env
|
||||
venv/
|
||||
node_modules
|
||||
tmp.py
|
||||
!/plugins/user/Dashboard
|
||||
/data/www/plugins/*
|
||||
!/data/www/plugins/.gitkeep
|
||||
/var/run/storage/*
|
||||
!/var/run/storage/.gitkeep
|
||||
*.egg-info
|
||||
/build/
|
||||
/dist/
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@ -1,11 +1,24 @@
|
||||
FROM python:3.9.17-alpine3.17
|
||||
FROM python:3.9-slim-bullseye
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev sqlite-dev build-base linux-headers
|
||||
# Install ffmpeg and other dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libc6-dev \
|
||||
sqlite3 \
|
||||
libsqlite3-dev \
|
||||
ntfs-3g \
|
||||
ffmpeg \
|
||||
build-essential \
|
||||
curl \
|
||||
tar \
|
||||
bash \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip install -r requirements.txt && apk del .build-deps gcc musl-dev sqlite-dev build-base linux-headers
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
ENTRYPOINT ["python", "/app/obscreen.py"]
|
||||
|
||||
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
docs/setup-run-on-rpi.md
|
||||
docs/setup-run-headless.md
|
||||
128
README.md
128
README.md
@ -1,44 +1,120 @@
|
||||
# <img src="https://github.com/jr-k/obscreen/blob/master/docs/img/obscreen.png" width="22"> Obscreen v2
|
||||
<div align="center" width="100%">
|
||||
<img src="./docs/img/obscreen.png" width="128" alt="" />
|
||||
</div>
|
||||
|
||||
# Obscreen
|
||||
|
||||
🧑🎄 Open to feature request and pull request
|
||||
Obscreen is a user-friendly self-hosted digital signage tool leveraging chromium browser.
|
||||
|
||||
**⭐️ You liked it ? Give this repository a star, it's free :)**
|
||||
<a target="_blank" href="https://git.sumisu.xyz/csmith1865/obscreen"><img src="https://img.shields.io/gitea/stars/csmith1865/obscreen?gitea_url=https%3A%2F%2Fgit.sumisu.xyz&style=flat" /></a> <a target="_blank" href="https://hub.docker.com/r/csmith1865/obscreen"><img src="https://img.shields.io/docker/pulls/csmith1865/obscreen" /></a> <a target="_blank" href="https://hub.docker.com/r/csmith1865/obscreen"><img src="https://img.shields.io/docker/v/csmith1865/obscreen/latest?label=docker%20image%20ver." /></a> <a target="_blank" href="https://git.sumisu.xyz/csmith1865/obscreen"><img src="https://img.shields.io/gitea/last-commit/csmith1865/obscreen?gitea_url=https%3A%2F%2Fgit.sumisu.xyz&style=flat" /></a>
|
||||
|
||||
## About
|
||||
Use a RaspberryPi (Lite OS) to show a fullscreen slideshow in a web browser (Kiosk-mode)
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-playlist-edit.png" width="700" alt="" />
|
||||
|
||||
[](https://hub.docker.com/r/jierka/obscreen/)
|
||||
🧑🎄 Open to feature request and pull request. [Cast your vote for your preferred ones on the Canny platform](https://obscreen.canny.io/feature-requests)
|
||||
|
||||
### Features:
|
||||
- Dead simple chromium webview
|
||||
- Clear GUI
|
||||
⭐️ You liked it ? Give this repository a star, it's free :)
|
||||
|
||||
---
|
||||
|
||||
## 🕹️ Live Demo
|
||||
|
||||
Try it!
|
||||
|
||||
Demo Server (Location: Roubaix - France): [https://demo.obscreen.io](https://demo.obscreen.io/login?username=admin&password=admin)
|
||||
|
||||
It is a temporary live demo, all data will be deleted after 30 minutes (~30secs downtime).
|
||||
|
||||
## 🎉 Features
|
||||
- Dead simple chromium webview inside
|
||||
- Fancy graphical user interface
|
||||
- Very few dependencies
|
||||
- SQLite database
|
||||
- Plugin system
|
||||
- Feature flags to enable complex use cases (Fleet/User/Playlist management)
|
||||
- No stupid pricing plan
|
||||
- No cloud
|
||||
- No telemetry
|
||||
- Embeddable SQLite database
|
||||
- Fleet screen management
|
||||
- Playlist management
|
||||
- Authentication management
|
||||
- Plays content from flashdrive in offline mode
|
||||
- Core API & Plugin system to extend capabilities
|
||||
- [Multi Languages](https://git.sumisu.xyz/csmith1865/obscreen/src/branch/master/lang)
|
||||
- Cast pictures and iframes to Chromecast
|
||||
- No costly monthly pricing plan per screen or whatever, no cloud, no telemetry
|
||||
|
||||

|
||||
|
||||
# Cookbooks
|
||||
## 👨🍳 How to install
|
||||
|
||||
### 🔴 [I want to power a RaspberryPi and automatically see my slideshow on a screen connected to it and manage the slideshow](docs/setup-run-on-rpi.md)
|
||||
### 🔵 [I just want a slideshow manager and I'll deal with screen and browser myself](docs/setup-run-headless.md)
|
||||
|
||||
# Discussion
|
||||
|
||||
## 📸 More Screenshots
|
||||
|
||||
Light Mode:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-light-mode.png" width="512" alt="" />
|
||||
|
||||
Content Explorer:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-content-explorer.png" width="512" alt="" />
|
||||
|
||||
Settings Page:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-settings.png" width="512" alt="" />
|
||||
|
||||
Add Content Modal:
|
||||
|
||||
<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/screenshot-add-content.png" width="512" alt="" />
|
||||
|
||||
## 🫡 Motivation
|
||||
|
||||
- I was searching for a self-hosted monitoring tool similar to "Screenly", but struggled with "Anthias" (formerly Screenly OSE) due to compatibility issues on some webpages. Chromium does a great job at rendering webpages, so I decided to create my own solution based on browsers.
|
||||
- Enjoy a beautiful graphical interface
|
||||
- My goal was to keep the code as simple as possible, using reliable technology with minimal dependencies.
|
||||
- Aim to showcase the power of the Raspberry Pi 5.
|
||||
- Deploy my first true Docker image to Docker Hub using a continuous deployment pipeline.
|
||||
|
||||
If you value this project, please think about awarding it a ⭐. Thanks ! 🙏
|
||||
|
||||
## 🗺️ Short-term roadmap
|
||||
- New `Composition` content type: Check out a [video demo here](https://demo.obscreen.io/data/uploads/compositions.mp4)
|
||||
- New `Text` Content Type: Display text with customizable styles, including options for scrolling effects.
|
||||
- New `HTML` Content Type: Display HTML snippets for more powerful text customization, giving you full control over the content.
|
||||
- Fleet Studio Management: Reviving a legacy feature
|
||||
- Remote Player Server: A new way to manage a player from the studio without needing SSH access to player
|
||||
|
||||
## 🛟 Discussion / Need help ?
|
||||
|
||||
### Join our Discord
|
||||
[<img src="https://github.com/jr-k/obscreen/blob/master/docs/img/discord.png" width="64">](https://discord.obscreen.io)
|
||||
[<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/img/discord.png" width="64">](https://discord.obscreen.io)
|
||||
|
||||
### Open an Issue or a Pull Request on Github
|
||||
[<img src="https://github.com/jr-k/obscreen/blob/master/docs/img/github.png" width="64">](https://github.com/jr-k/obscreen/issues/new/choose)
|
||||
### Open an Issue
|
||||
[<img src="https://git.sumisu.xyz/csmith1865/obscreen/raw/branch/master/docs/img/github.png" width="64">](https://git.sumisu.xyz/csmith1865/obscreen/issues/new/choose)
|
||||
|
||||
# Troubleshoot
|
||||
### Troubleshoot
|
||||
|
||||
|
||||
<details closed>
|
||||
<summary><h3>Why aren't the videos starting?</h3></summary>
|
||||
|
||||
### Videos aren't playing why ?
|
||||
This is "normal" behavior. Videos do not play automatically in Chrome because it requires user interaction with the page (a simple click inside the webpage is enough). If you open the console, you'll see the error: [Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first...](https://goo.gl/xX8pDD)
|
||||
|
||||
To resolve this, you need to use the Chrome flag --autoplay-policy=no-user-gesture-required. When connecting a Raspberry Pi with Obscreen Player autorun, this issue doesn't occur because the flag is handled automatically for you.You need to enable this flag yourself otherwise.
|
||||
To resolve this, you need to use the Chrome flag `--autoplay-policy=no-user-gesture-required`. When connecting a Raspberry Pi with Obscreen Player autorun, this issue doesn't occur because the flag is handled automatically for you. You need to enable this flag yourself otherwise.
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
## 👑 Contributions
|
||||
|
||||
### Create Pull Requests
|
||||
|
||||
We accept all types of pull requests.
|
||||
|
||||
### Test Beta Version
|
||||
|
||||
Check out the latest beta release here: https://github.com/jr-k/obscreen/releases
|
||||
|
||||
### Translations
|
||||
|
||||
If you want to translate Obscreen into your language, please visit [Languages Files](https://git.sumisu.xyz/csmith1865/obscreen/src/branch/master/lang).
|
||||
|
||||
### Spelling & Grammar
|
||||
|
||||
Feel free to correct the grammar in the documentation or code.
|
||||
My mother language is not English and my grammar is not that great.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,6 +1,8 @@
|
||||
jQuery(document).ready(function ($) {
|
||||
const main = function () {
|
||||
|
||||
$('.user-token-reveal').each(function() {
|
||||
updateTokenReveal($(this), false);
|
||||
});
|
||||
};
|
||||
|
||||
$(document).on('click', '.user-add', function () {
|
||||
@ -17,5 +19,26 @@ jQuery(document).ready(function ($) {
|
||||
$('#user-edit-id').val(user.id);
|
||||
});
|
||||
|
||||
const updateTokenReveal = function($btn, revealState) {
|
||||
const $holder = $btn.parents('.user-item:eq(0)');
|
||||
const $input = $holder.find('.input-token:eq(0)');
|
||||
const $icon = $btn.find('i:eq(0)');
|
||||
const isActive = revealState !== undefined ? !revealState : $icon.hasClass('fa-eye-slash');
|
||||
|
||||
if (isActive) {
|
||||
$icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
$btn.removeClass('btn-neutral').addClass('btn-other');
|
||||
$input.val($input.attr('data-private'));
|
||||
} else {
|
||||
$icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
$btn.removeClass('btn-other').addClass('btn-neutral');
|
||||
$input.val($input.attr('data-public'));
|
||||
}
|
||||
};
|
||||
|
||||
$(document).on('click', '.user-token-reveal', function () {
|
||||
updateTokenReveal($(this));
|
||||
});
|
||||
|
||||
main();
|
||||
});
|
||||
74
data/www/js/cast-url.js
Normal file
74
data/www/js/cast-url.js
Normal file
@ -0,0 +1,74 @@
|
||||
var applicationID = '81585E3E';
|
||||
var namespace = 'urn:x-cast:com.jrk.obscreen';
|
||||
var session = null;
|
||||
|
||||
if (!chrome.cast || !chrome.cast.isAvailable) {
|
||||
setTimeout(initializeCastApi, 1000);
|
||||
}
|
||||
|
||||
function initializeCastApi() {
|
||||
var sessionRequest = new chrome.cast.SessionRequest(applicationID);
|
||||
var apiConfig = new chrome.cast.ApiConfig(sessionRequest,
|
||||
sessionListener,
|
||||
receiverListener);
|
||||
|
||||
chrome.cast.initialize(apiConfig, onInitSuccess, onError);
|
||||
}
|
||||
|
||||
function onInitSuccess() {
|
||||
// console.log('onInitSuccess');
|
||||
}
|
||||
|
||||
function onError(message) {
|
||||
console.error('onError: ' + JSON.stringify(message));
|
||||
}
|
||||
|
||||
function onSuccess(message) {
|
||||
// console.log('onSuccess: ' + JSON.stringify(message));
|
||||
}
|
||||
|
||||
// function onStopAppSuccess() {
|
||||
// console.log('onStopAppSuccess');
|
||||
// }
|
||||
|
||||
function sessionListener(e) {
|
||||
console.log('New session ID: ' + e.sessionId);
|
||||
session = e;
|
||||
session.addUpdateListener(sessionUpdateListener);
|
||||
}
|
||||
|
||||
function sessionUpdateListener(isAlive) {
|
||||
console.log((isAlive ? 'Session Updated' : 'Session Removed') + ': ' + session.sessionId);
|
||||
if (!isAlive) {
|
||||
session = null;
|
||||
}
|
||||
}
|
||||
|
||||
function receiverListener(e) {
|
||||
// Due to API changes just ignore this.
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
if (session != null) {
|
||||
session.sendMessage(namespace, message, onSuccess.bind(this, message), onError);
|
||||
} else {
|
||||
chrome.cast.requestSession(function (e) {
|
||||
session = e;
|
||||
sessionListener(e);
|
||||
session.sendMessage(namespace, message, onSuccess.bind(this, message), onError);
|
||||
}, onError);
|
||||
}
|
||||
}
|
||||
|
||||
// function stopApp() {
|
||||
// session.stop(onStopAppSuccess, onError);
|
||||
// }
|
||||
|
||||
jQuery(function ($) {
|
||||
$(document).on('click', '.cast-url', function () {
|
||||
sendMessage({
|
||||
type: 'load',
|
||||
url: $('#' + $(this).attr('data-target-id')).val()
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -10,13 +10,24 @@ jQuery(document).ready(function ($) {
|
||||
});
|
||||
|
||||
$(document).on('click', '.node-player-group-preview', function () {
|
||||
const $iframe = $('<iframe>', {
|
||||
src: $(this).attr('data-url'),
|
||||
frameborder: 0
|
||||
});
|
||||
const $icon = $(this).find('i');
|
||||
const isPlay = $icon.hasClass('fa-play');
|
||||
const $holder = $(this).parents('.preview:eq(0)');
|
||||
|
||||
$(this).parents('.preview:eq(0)').append($iframe);
|
||||
$(this).remove();
|
||||
if (isPlay) {
|
||||
const $iframe = $('<iframe>', {
|
||||
src: $(this).attr('data-url'),
|
||||
frameborder: 0
|
||||
});
|
||||
|
||||
$holder.append($iframe);
|
||||
$(this).addClass('hover-only');
|
||||
$icon.removeClass('fa-play').addClass('fa-pause');
|
||||
} else {
|
||||
$holder.find('iframe').remove();
|
||||
$(this).removeClass('hover-only');
|
||||
$icon.removeClass('fa-pause').addClass('fa-play');
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.node-player-group-player-assign', function () {
|
||||
|
||||
@ -30,6 +30,10 @@ const hideDropdowns = function () {
|
||||
$('.dropdown').removeClass('dropdown-show');
|
||||
};
|
||||
|
||||
const classColorXor = function(color, fallback) {
|
||||
return color === 'gscaleF' ? 'gscale0' : (color === 'gscale0' ? 'gscaleF' : fallback);
|
||||
};
|
||||
|
||||
const showToast = function (text) {
|
||||
const $toast = $(".toast");
|
||||
$toast.addClass('show');
|
||||
@ -162,5 +166,12 @@ jQuery(document).ready(function ($) {
|
||||
|
||||
showToast(l.js_common_copied);
|
||||
});
|
||||
|
||||
$(window).on('beforeunload', function(event) {
|
||||
$('.modal').each(function() {
|
||||
$(this).find('button[type=submit]').removeClass('hidden');
|
||||
$(this).find('.btn-loading').addClass('hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
13
data/www/js/lib/cast-sender.js
Normal file
13
data/www/js/lib/cast-sender.js
Normal file
@ -0,0 +1,13 @@
|
||||
(function(){/*
|
||||
|
||||
Copyright The Closure Library Authors.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
'use strict';var l=function(){var a=h,b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}},m=this||self,n=/^[\w+/_-]+[=]{0,2}$/,p=null,q=function(a){return(a=a.querySelector&&a.querySelector("script[nonce]"))&&(a=a.nonce||a.getAttribute("nonce"))&&n.test(a)?a:""},r=function(a,b){function e(){}e.prototype=b.prototype;a.i=b.prototype;a.prototype=new e;a.prototype.constructor=a;a.h=function(c,g,k){for(var f=Array(arguments.length-2),d=2;d<arguments.length;d++)f[d-2]=arguments[d];
|
||||
return b.prototype[g].apply(c,f)}},t=function(a){return a};function u(a){if(Error.captureStackTrace)Error.captureStackTrace(this,u);else{var b=Error().stack;b&&(this.stack=b)}a&&(this.message=String(a))}r(u,Error);u.prototype.name="CustomError";var v=function(a,b){a=a.split("%s");for(var e="",c=a.length-1,g=0;g<c;g++)e+=a[g]+(g<b.length?b[g]:"%s");u.call(this,e+a[c])};r(v,u);v.prototype.name="AssertionError";var w=function(a,b){throw new v("Failure"+(a?": "+a:""),Array.prototype.slice.call(arguments,1));};var x;var A=function(a,b){this.g=b===z?a:""};A.prototype.toString=function(){return this.g+""};var z={};var B=function(){var a=window.navigator.userAgent.match(/Chrome\/([0-9]+)/);return a?parseInt(a[1],10):0},C=function(a){return!!document.currentScript&&(-1!=document.currentScript.src.indexOf("?"+a)||-1!=document.currentScript.src.indexOf("&"+a))},D=function(){return"function"==typeof window.__onGCastApiAvailable?window.__onGCastApiAvailable:null},F=function(a){a.length?E(a.shift(),function(){F(a)}):G()},H=function(a){return"chrome-extension://"+a+"/cast_sender.js"},E=function(a,b,e){var c=document.createElement("script");
|
||||
c.onerror=b;e&&(c.onload=e);if(void 0===x)if(b=null,(e=m.trustedTypes)&&e.createPolicy){try{b=e.createPolicy("goog#html",{createHTML:t,createScript:t,createScriptURL:t})}catch(y){m.console&&m.console.error(y.message)}x=b}else x=b;a=(b=x)?b.createScriptURL(a):a;a=new A(a,z);a:{try{var g=c&&c.ownerDocument,k=g&&(g.defaultView||g.parentWindow);k=k||m;if(k.Element&&k.Location){var f=k;break a}}catch(y){}f=null}if(f&&"undefined"!=typeof f.HTMLScriptElement&&(!c||!(c instanceof f.HTMLScriptElement)&&(c instanceof
|
||||
f.Location||c instanceof f.Element))){f=typeof c;if("object"==f&&null!=c||"function"==f)try{var d=c.constructor.displayName||c.constructor.name||Object.prototype.toString.call(c)}catch(y){d="<object could not be stringified>"}else d=void 0===c?"undefined":null===c?"null":typeof c;w("Argument is not a %s (or a non-Element, non-Location mock); got: %s","HTMLScriptElement",d)}a instanceof A&&a.constructor===A?d=a.g:(d=typeof a,w("expected object of type TrustedResourceUrl, got '"+a+"' of type "+("object"!=
|
||||
d?d:a?Array.isArray(a)?"array":d:"null")),d="type_error:TrustedResourceUrl");c.src=d;(d=c.ownerDocument&&c.ownerDocument.defaultView)&&d!=m?d=q(d.document):(null===p&&(p=q(m.document)),d=p);d&&c.setAttribute("nonce",d);(document.head||document.documentElement).appendChild(c)},I=function(){var a=B(),b=[];if(1<a){var e=a-1;b.push("https://www.gstatic.com/eureka/clank/"+a+"/cast_sender.js");b.push("https://www.gstatic.com/eureka/clank/"+e+"/cast_sender.js")}return b},G=function(){var a=D();a&&a(!1,"No cast extension found")},
|
||||
K=function(){if(J){var a=2,b=D(),e=function(){a--;0==a&&b&&b(!0)};window.__onGCastApiAvailable=e;E("https://www.gstatic.com/cast/sdk/libs/sender/1.0/cast_framework.js",G,e)}},J=C("loadCastFramework")||C("loadCastApplicationFramework"),L=["pkedcjkdefgpdelpbcmbmeomcjbeemfm","enhhojjnijigcajfphajepfemndkmdlo"];if(0<=window.navigator.userAgent.indexOf("Android")&&0<=window.navigator.userAgent.indexOf("Chrome/")&&window.navigator.presentation){if(60<=B()){K();var M=I();M.push("https://www.gstatic.com/eureka/clank/cast_sender.js");F(M)}}else if(!window.chrome||!window.navigator.presentation||0<=window.navigator.userAgent.indexOf("Edge"))G();else if(89<=B()){K();var N=I(),O=N.push,P=O.apply,h=L.map(H),Q;if(h instanceof Array)Q=h;else{var R,S="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];R=S?S.call(h):
|
||||
{next:l()};for(var T,U=[];!(T=R.next()).done;)U.push(T.value);Q=U}P.call(O,N,Q);N.push("https://www.gstatic.com/eureka/clank/cast_sender.js");F(N)}else K(),F(L.map(H));}).call(this);
|
||||
|
||||
84
data/www/js/lib/jquery-more.js
vendored
Normal file
84
data/www/js/lib/jquery-more.js
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
jQuery(function () {
|
||||
$(document).ready(function () {
|
||||
function adjustValue(inputElement, delta) {
|
||||
const currentValue = parseInt(inputElement.value) || 0;
|
||||
const newValue = currentValue + delta;
|
||||
if (("" + newValue).length <= inputElement.maxLength) {
|
||||
inputElement.value = newValue >= 0 ? newValue : 0;
|
||||
$(inputElement).trigger('input');
|
||||
}
|
||||
}
|
||||
|
||||
$('.numeric-input').on('input', function () {
|
||||
this.value = this.value.replace(/[^0-9]/g, '');
|
||||
});
|
||||
|
||||
$('.numeric-input').on('keydown', function (e) {
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
adjustValue(this, e.shiftKey ? 10 : 1);
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
adjustValue(this, e.shiftKey ? -10 : -1);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function updateRadioActiveClass() {
|
||||
$('.radio-group label').removeClass('active');
|
||||
$('input[type="radio"]:checked').next('label').addClass('active');
|
||||
}
|
||||
updateRadioActiveClass();
|
||||
$('.radio-group input[type="radio"]').change(function() {
|
||||
updateRadioActiveClass();
|
||||
});
|
||||
|
||||
|
||||
function updateCheckboxActiveClass() {
|
||||
$('.checkbox-group label').each(function() {
|
||||
const checkbox = $(this).prev('input[type="checkbox"]');
|
||||
if (checkbox.is(':checked')) {
|
||||
$(this).addClass('active');
|
||||
} else {
|
||||
$(this).removeClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
updateCheckboxActiveClass();
|
||||
$('.checkbox-group input[type="checkbox"]').change(function() {
|
||||
updateCheckboxActiveClass();
|
||||
});
|
||||
|
||||
$.fn.serializeObject = function() {
|
||||
const obj = {};
|
||||
|
||||
this.find('input, select, textarea').each(function() {
|
||||
const field = $(this);
|
||||
const name = field.attr('name');
|
||||
|
||||
if (!name) return; // Ignore fields without a name
|
||||
|
||||
if (field.is(':checkbox')) {
|
||||
const isOnOff = field.val() === 'on' || field.val() === '1';
|
||||
obj[name] = field.is(':checked') ? field.val() : (isOnOff ? false : null);
|
||||
} else if (field.is(':radio')) {
|
||||
if (field.is(':checked')) {
|
||||
obj[name] = field.val();
|
||||
} else if (!(name in obj)) {
|
||||
obj[name] = false;
|
||||
}
|
||||
} else {
|
||||
const tryInt = parseInt(field.val());
|
||||
obj[name] = isNaN(tryInt) ? field.val() : tryInt;
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
1
data/www/js/lib/jquery-ui-rotatable.min.js
vendored
Normal file
1
data/www/js/lib/jquery-ui-rotatable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
data/www/js/lib/jscolor.min.js
vendored
Normal file
1
data/www/js/lib/jscolor.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -21,14 +21,81 @@ jQuery(document).ready(function ($) {
|
||||
});
|
||||
|
||||
$(document).on('click', '.playlist-preview', function () {
|
||||
const $iframe = $('<iframe>', {
|
||||
src: $(this).attr('data-url'),
|
||||
frameborder: 0
|
||||
});
|
||||
const $icon = $(this).find('i');
|
||||
const isPlay = $icon.hasClass('fa-play');
|
||||
const $holder = $(this).parents('.preview:eq(0)');
|
||||
|
||||
$(this).parents('.preview:eq(0)').append($iframe);
|
||||
$(this).remove();
|
||||
if (isPlay) {
|
||||
const $iframe = $('<iframe>', {
|
||||
src: $(this).attr('data-url'),
|
||||
frameborder: 0
|
||||
});
|
||||
|
||||
$holder.append($iframe);
|
||||
$(this).addClass('hover-only');
|
||||
$icon.removeClass('fa-play').addClass('fa-pause');
|
||||
} else {
|
||||
$holder.find('iframe').remove();
|
||||
$(this).removeClass('hover-only');
|
||||
$icon.removeClass('fa-pause').addClass('fa-play');
|
||||
}
|
||||
});
|
||||
//
|
||||
// $(document).on('click', '.cast-scan', function () {
|
||||
// showModal('modal-playlist-cast-scan');
|
||||
// const $modal = $('.modal-playlist-cast-scan:visible');
|
||||
// const $holder = $modal.find('.cast-devices');
|
||||
// const $loading = $modal.find('.loading');
|
||||
//
|
||||
// $loading.removeClass('hidden');
|
||||
// $holder.removeClass('hidden');
|
||||
// $holder.html('');
|
||||
// $loading.html($loading.attr('data-loading'));
|
||||
//
|
||||
// $.ajax({
|
||||
// method: 'GET',
|
||||
// url: route_cast_scan,
|
||||
// headers: {'Content-Type': 'application/json'},
|
||||
// success: function (response) {
|
||||
// $loading.addClass('hidden')
|
||||
//
|
||||
// for (let i = 0; i < response.devices.length; i++) {
|
||||
// const device = response.devices[i];
|
||||
// $holder.append($('<li><a href="javascript:void(0)" class="cast-device" data-id="' + device.friendly_name + '"><i class="fa fa-brands fa-chromecast"></i>' + device.friendly_name + '</a></li>'));
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
// $(document).on('click', '.cast-device', function () {
|
||||
// const $modal = $('.modal-playlist-cast-scan:visible');
|
||||
// const $holder = $modal.find('.cast-devices');
|
||||
// const $loading = $modal.find('.loading');
|
||||
//
|
||||
// $holder.addClass('hidden');
|
||||
// $loading.removeClass('hidden');
|
||||
// $loading.html($loading.attr('data-casting'));
|
||||
//
|
||||
// const id = $(this).attr('data-id');
|
||||
//
|
||||
// $.ajax({
|
||||
// url: route_cast_url,
|
||||
// method: 'POST',
|
||||
// data: JSON.stringify({
|
||||
// device: id,
|
||||
// url: $('#playlist-preview-url').val()
|
||||
// }),
|
||||
// headers: {'Content-Type': 'application/json'},
|
||||
// success: function (response) {
|
||||
// $loading.addClass('hidden');
|
||||
// hideModal();
|
||||
// },
|
||||
// error: function () {
|
||||
// $loading.addClass('hidden');
|
||||
// $holder.removeClass('hidden');
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
main();
|
||||
});
|
||||
|
||||
@ -22,7 +22,7 @@ jQuery(document).ready(function ($) {
|
||||
$('.modal-variable-edit input:visible:eq(0)').focus().select();
|
||||
$('#variable-edit-name').val(variable.name);
|
||||
$('#variable-edit-description').html(variable.description);
|
||||
$('#variable-edit-description-edition').html(variable.description_edition).toggleClass('hidden', variable.description_edition === '');
|
||||
$('#variable-edit-description-edition').html(variable.description_edition).toggleClass('hidden', variable.description_edition === '' || variable.description_edition === null);
|
||||
$('#variable-edit-value').val(variable.value);
|
||||
$('#variable-edit-id').val(variable.id);
|
||||
});
|
||||
|
||||
@ -9,7 +9,7 @@ jQuery(document).ready(function ($) {
|
||||
if (confirm(l.js_sysinfo_restart_confirmation)) {
|
||||
$('body').html($('<div class="reboot">').html(l.js_sysinfo_restart_loading)).css({margin:200});
|
||||
$.ajax({
|
||||
url: '/sysinfo/restart?secret_key='+secret_key,
|
||||
url: route_sysinfo_restart + '?secret_key='+secret_key,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
data: '',
|
||||
method: 'POST',
|
||||
|
||||
@ -23,7 +23,7 @@ jQuery(document).ready(function ($) {
|
||||
$('#variable-edit-name').val(variable.name);
|
||||
$('#variable-edit-description').html(variable.description);
|
||||
$('#variable-edit-description-edition').html(variable.description_edition).toggleClass('hidden', variable.description_edition == '');
|
||||
$('#variable-edit-value').val(variable.value);
|
||||
$('#variable-edit-value').prop('required', variable.type !== 'string').val(variable.value);
|
||||
$('#variable-edit-id').val(variable.id);
|
||||
});
|
||||
|
||||
|
||||
471
data/www/js/slideshow/content-composition.js
Normal file
471
data/www/js/slideshow/content-composition.js
Normal file
@ -0,0 +1,471 @@
|
||||
|
||||
jQuery(document).ready(function ($) {
|
||||
const DEFAULT_RATIO = "16/9";
|
||||
const contentData = JSON.parse($('#content-edit-location').val() || `{"ratio":"${DEFAULT_RATIO}", "layers":{}}`);
|
||||
let currentElement = null;
|
||||
let elementCounter = 0;
|
||||
let screenRatio = 16/9;
|
||||
|
||||
const setRatio = function () {
|
||||
const ratioString = $('#elem-screen-ratio').val() || DEFAULT_RATIO;
|
||||
$('.ratio-value').text(ratioString.replace('/', ' / '));
|
||||
screenRatio = evalStringRatio(ratioString);
|
||||
$('.screen-holder').css({ 'padding-top': ( 1/ ( screenRatio ) * 100) + '%' });
|
||||
$('.ratio-value').val(screenRatio);
|
||||
$('#screen').css({
|
||||
width: $('#screen').width(),
|
||||
height: $('#screen').width() * (1/screenRatio),
|
||||
position: 'relative',
|
||||
}).parents('.screen-holder:eq(0)').css({
|
||||
width: 'auto',
|
||||
'padding-top': '0px'
|
||||
});
|
||||
};
|
||||
setRatio();
|
||||
|
||||
$(document).on('input', '#elem-screen-ratio', function() {
|
||||
setRatio();
|
||||
});
|
||||
|
||||
function createElement(config = null) {
|
||||
const screen = $('#screen');
|
||||
const screenWidth = screen.width();
|
||||
const screenHeight = screen.height();
|
||||
|
||||
const elementWidth = config ? (config.widthPercent / 100) * screenWidth : 100;
|
||||
const elementHeight = config ? (config.heightPercent / 100) * screenHeight : 50;
|
||||
let x = config ? (config.xPercent / 100) * screenWidth : Math.round(Math.random() * (screenWidth - elementWidth));
|
||||
let y = config ? (config.yPercent / 100) * screenHeight : Math.round(Math.random() * (screenHeight - elementHeight));
|
||||
const zIndex = config ? config.zIndex : elementCounter++;
|
||||
|
||||
//x = Math.round(Math.max(0, Math.min(x, screenWidth - elementWidth)));
|
||||
//y = Math.round(Math.max(0, Math.min(y, screenHeight - elementHeight)));
|
||||
|
||||
const elementId = zIndex;
|
||||
const element = $('<div class="element" id="element-' + zIndex + '" data-id="' + zIndex + '"><i class="fa fa-cog"></i></div>');
|
||||
// const element = $('<div class="element" id="' + elementId + '"><button>Button</button><div class="rotate-handle"></div></div>');
|
||||
|
||||
element.css({
|
||||
left: x,
|
||||
top: y,
|
||||
width: elementWidth,
|
||||
height: elementHeight,
|
||||
zIndex: zIndex,
|
||||
transform: `rotate(0deg)`
|
||||
});
|
||||
|
||||
element.draggable({
|
||||
// containment: "#screen",
|
||||
start: function (event, ui) {
|
||||
focusElement(ui.helper);
|
||||
},
|
||||
drag: function (event, ui) {
|
||||
updateForm(ui.helper);
|
||||
}
|
||||
});
|
||||
|
||||
element.resizable({
|
||||
// containment: "#screen",
|
||||
handles: 'n, s, e, w, nw, ne, sw, se',
|
||||
start: function (event, ui) {
|
||||
focusElement(ui.element);
|
||||
},
|
||||
resize: function (event, ui) {
|
||||
updateForm(ui.element);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
element.rotatable({
|
||||
handle: element.find('.rotate-handle'),
|
||||
rotate: function(event, ui) {
|
||||
updateForm(ui.element);
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
element.click(function () {
|
||||
focusElement($(this));
|
||||
});
|
||||
|
||||
screen.append(element);
|
||||
addElementToList(elementId);
|
||||
|
||||
if (config !== null && config.contentId !== null) {
|
||||
element.attr('data-content-id', config.contentId);
|
||||
element.attr('data-content-name', config.contentName);
|
||||
element.attr('data-content-type', config.contentType);
|
||||
element.attr('data-content-metadata', config.contentMetadata);
|
||||
|
||||
applyContentToElement({
|
||||
id: config.contentId,
|
||||
name: config.contentName,
|
||||
type: config.contentType,
|
||||
metadata: config.contentMetadata,
|
||||
}, element);
|
||||
|
||||
updateForm(element);
|
||||
unfocusElements();
|
||||
} else {
|
||||
setTimeout(function () {
|
||||
focusElement(element);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
$(document).on('click', '.element-adjust-aspect-ratio', function(){
|
||||
const metadata = currentElement.data('content-metadata');
|
||||
const ratio = metadata.height / metadata.width;
|
||||
$('#elem-height').val($('#elem-width').val() * ratio).trigger('input');
|
||||
$('#elem-width').val($('#elem-width').val()).trigger('input');
|
||||
});
|
||||
|
||||
$(document).on('click', '.element-list-item', function(){
|
||||
focusElement($('#element-' + $(this).attr('data-id')));
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove-element', function(){
|
||||
if (confirm(l.js_common_are_you_sure)) {
|
||||
removeElementById($(this).attr('data-id'));
|
||||
}
|
||||
});
|
||||
|
||||
function removeElementById(elementId) {
|
||||
$('.element[data-id='+elementId+'], .element-list-item[data-id='+elementId+']').remove();
|
||||
updateZIndexes();
|
||||
}
|
||||
|
||||
function addElementToList(elementId) {
|
||||
const listItem = `<div class="element-list-item" data-id="__ID__">
|
||||
<i class="fa fa-cog"></i>
|
||||
<div class="inner">
|
||||
<label>__EMPTY__ __ID__ </label>
|
||||
<button type="button" class="btn btn-naked remove-element" data-id="__ID__">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-neutral configure-element content-explr-picker" data-id="__ID__">
|
||||
<i class="fa fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
$('#elementList').append(
|
||||
$(listItem
|
||||
.replace(/__ID__/g, elementId)
|
||||
.replace(/__EMPTY__/g, l.js_common_empty)
|
||||
)
|
||||
);
|
||||
updateZIndexes();
|
||||
}
|
||||
|
||||
function unfocusElements() {
|
||||
$('.element, .element-list-item').removeClass('focused');
|
||||
currentElement = null;
|
||||
updateForm(null);
|
||||
}
|
||||
|
||||
function focusElement($element) {
|
||||
unfocusElements();
|
||||
currentElement = $element;
|
||||
$element.addClass('focused');
|
||||
const listElement = $('.element-list-item[data-id="' + $element.attr('data-id') + '"]');
|
||||
listElement.addClass('focused');
|
||||
updateForm($element);
|
||||
|
||||
const contentType = $element.attr('data-content-type');
|
||||
$('.element-tool').addClass('hidden');
|
||||
|
||||
if (contentType) {
|
||||
if (contentType === 'picture' || contentType === 'video') {
|
||||
const contentMetadata = $element.data('content-metadata');
|
||||
if (contentMetadata.width && contentMetadata.height) {
|
||||
$('.element-tool.element-adjust-aspect-ratio-container').removeClass('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateForm($element) {
|
||||
if (!$element) {
|
||||
$('form#elementForm input').val('').prop('disabled', true);
|
||||
$('.form-element-properties').addClass('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$('.form-element-properties').removeClass('hidden');
|
||||
$('form#elementForm input').prop('disabled', false);
|
||||
|
||||
const offset = $element.position();
|
||||
|
||||
if (offset !== undefined) {
|
||||
$('#elem-x').val(offset.left);
|
||||
$('#elem-y').val(offset.top);
|
||||
$('#elem-width').val($element.width());
|
||||
$('#elem-height').val($element.height());
|
||||
}
|
||||
|
||||
$element.find('i').css('font-size', Math.min($element.width(), $element.height()) / 3);
|
||||
|
||||
/*
|
||||
const rotation = $element.css('transform');
|
||||
const values = rotation.split('(')[1].split(')')[0].split(',');
|
||||
const angle = Math.round(Math.atan2(values[1], values[0]) * (180/Math.PI));
|
||||
$('#elem-rotate').val(angle);
|
||||
*/
|
||||
}
|
||||
|
||||
$(document).on('input', '#elementForm input', function () {
|
||||
if (!currentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screenWidth = $('#screen').width();
|
||||
const screenHeight = $('#screen').height();
|
||||
|
||||
let x = Math.round(parseInt($('#elem-x').val()));
|
||||
let y = Math.round(parseInt($('#elem-y').val()));
|
||||
let width = Math.round(parseInt($('#elem-width').val()));
|
||||
let height = Math.round(parseInt($('#elem-height').val()));
|
||||
// let rotation = parseInt($('#elem-rotate').val());
|
||||
|
||||
// Constrain x and y
|
||||
// x = Math.max(0, Math.min(x, screenWidth - width));
|
||||
// y = Math.max(0, Math.min(y, screenHeight - height));
|
||||
|
||||
// Constrain width and height
|
||||
width = Math.min(width, screenWidth - x);
|
||||
height = Math.min(height, screenHeight - y);
|
||||
|
||||
currentElement.css({
|
||||
left: x,
|
||||
top: y,
|
||||
width: width,
|
||||
height: height
|
||||
// transform: `rotate(${rotation}deg)`
|
||||
});
|
||||
|
||||
// Update form values to reflect clamped values
|
||||
$('#elem-x').val(x);
|
||||
$('#elem-y').val(y);
|
||||
$('#elem-width').val(width);
|
||||
$('#elem-height').val(height);
|
||||
});
|
||||
|
||||
// $(document).on('click', '#addElement', function () {
|
||||
// createElement();
|
||||
// });
|
||||
|
||||
$(document).on('click', '#removeAllElements', function () {
|
||||
if (confirm(l.js_common_are_you_sure)) {
|
||||
$('.element, .element-list-item').remove();
|
||||
updateZIndexes();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('dblclick', '.element', function (e) {
|
||||
$('.content-explr-picker[data-id='+$(this).attr('data-id')+']').click();
|
||||
});
|
||||
|
||||
$(document).on('mousedown', function (e) {
|
||||
const keepFocusedElement = $(e.target).hasClass('element')
|
||||
|| $(e.target).hasClass('element-list-item')
|
||||
|| $(e.target).parents('.element:eq(0)').length !== 0
|
||||
|| $(e.target).parents('.element-list-item:eq(0)').length !== 0
|
||||
|| $(e.target).is('input,select,textarea')
|
||||
|| $(e.target).is('.page-panel.right-panel button,a,.btn')
|
||||
|
||||
if (!keepFocusedElement) {
|
||||
unfocusElements();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '#presetGrid2x2', function () {
|
||||
const screenWidth = $('#screen').width();
|
||||
const screenHeight = $('#screen').height();
|
||||
|
||||
let elements = $('.element');
|
||||
if (elements.length < 4) {
|
||||
while (elements.length < 4) {
|
||||
createElement();
|
||||
elements = $('.element');
|
||||
}
|
||||
}
|
||||
|
||||
elements = $('.element-list-item').map(function() {
|
||||
return $('.element[data-id='+$(this).attr('data-id')+']');
|
||||
}).slice(0, 4);
|
||||
|
||||
const gridPositions = [
|
||||
{x: 0, y: 0},
|
||||
{x: screenWidth / 2, y: 0},
|
||||
{x: 0, y: screenHeight / 2},
|
||||
{x: screenWidth / 2, y: screenHeight / 2}
|
||||
];
|
||||
|
||||
elements.each(function (index) {
|
||||
const position = gridPositions[index];
|
||||
$(this).css({
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
width: screenWidth / 2,
|
||||
height: screenHeight / 2
|
||||
});
|
||||
updateForm($(this));
|
||||
});
|
||||
|
||||
unfocusElements();
|
||||
});
|
||||
|
||||
$(document).on('click', '#presetTvNews1x1', function () {
|
||||
const screenWidth = $('#screen').width();
|
||||
const screenHeight = $('#screen').height();
|
||||
|
||||
let elements = $('.element');
|
||||
if (elements.length === 0) {
|
||||
createElement();
|
||||
}
|
||||
|
||||
if (!currentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const height = (screenHeight / 7);
|
||||
currentElement.css({
|
||||
left: 0,
|
||||
top: screenHeight - height,
|
||||
width: screenWidth,
|
||||
height: height
|
||||
});
|
||||
updateForm(currentElement);
|
||||
unfocusElements();
|
||||
});
|
||||
|
||||
$(document).keydown(function (e) {
|
||||
if (e.key === "Escape") {
|
||||
unfocusElements();
|
||||
}
|
||||
|
||||
const hasFocusInInput = $('input,textarea').is(':focus');
|
||||
|
||||
if (!currentElement || hasFocusInInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowLeft") {
|
||||
$('#elem-x').val(parseInt($('#elem-x').val()) - (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "ArrowRight") {
|
||||
$('#elem-x').val(parseInt($('#elem-x').val()) + (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "ArrowUp") {
|
||||
$('#elem-y').val(parseInt($('#elem-y').val()) - (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "ArrowDown") {
|
||||
$('#elem-y').val(parseInt($('#elem-y').val()) + (e.shiftKey ? 10 : 1)).trigger('input');
|
||||
} else if (e.key === "Backspace") {
|
||||
if (confirm(l.js_common_are_you_sure)) {
|
||||
removeElementById(currentElement.attr('data-id'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.content-explr-picker', function () {
|
||||
const elementId = $(this).attr('data-id');
|
||||
const isNew = !elementId;
|
||||
const $element = isNew ? $(createElement()) : $('#element-'+elementId);
|
||||
|
||||
showPickers('modal-content-explr-picker', function (content) {
|
||||
applyContentToElement(content, $element)
|
||||
});
|
||||
});
|
||||
|
||||
const applyContentToElement = function (content, $element) {
|
||||
$element.attr('data-content-id', content.id);
|
||||
$element.attr('data-content-name', content.name);
|
||||
$element.attr('data-content-type', content.type);
|
||||
$element.data('content-metadata', content.metadata);
|
||||
const $elementList = $('.element-list-item[data-id='+$element.attr('data-id')+']');
|
||||
const iconClasses = [
|
||||
'fa',
|
||||
content_type_icon_classes[content.type],
|
||||
content_type_color_classes[content.type]
|
||||
].join(' ');
|
||||
$element.find('i').get(0).classList = iconClasses;
|
||||
$elementList.find('label').text(content.name);
|
||||
$elementList.find('i:eq(0)').get(0).classList = iconClasses;
|
||||
};
|
||||
|
||||
$(document).on('submit', 'form.form', function (e) {
|
||||
unfocusElements();
|
||||
const location = getLocationPayload();
|
||||
$('#content-edit-location').val(JSON.stringify(location));
|
||||
});
|
||||
|
||||
function updateZIndexes() {
|
||||
const zindex = $('.element-list-item').length + 1;
|
||||
$('.element-list-item').each(function(index) {
|
||||
const id = $(this).attr('data-id');
|
||||
$('#element-' + id).css('z-index', zindex - index);
|
||||
});
|
||||
}
|
||||
|
||||
$('#elementList').sortable({
|
||||
update: function(event, ui) {
|
||||
updateZIndexes();
|
||||
}
|
||||
});
|
||||
|
||||
const applyElementsFromContent = function() {
|
||||
for (let i = 0; i < contentData.layers.length; i++) {
|
||||
createElement(contentData.layers[i]);
|
||||
}
|
||||
};
|
||||
|
||||
applyElementsFromContent();
|
||||
|
||||
const getLocationPayload = function() {
|
||||
const screen = $('#screen');
|
||||
const screenWidth = screen.width();
|
||||
const screenHeight = screen.height();
|
||||
const layers = [];
|
||||
|
||||
$('.element').each(function () {
|
||||
const $element = $(this);
|
||||
const offset = $element.position();
|
||||
const x = offset.left;
|
||||
const y = offset.top;
|
||||
const width = $element.width();
|
||||
const height = $element.height();
|
||||
|
||||
const xPercent = (x / screenWidth) * 100;
|
||||
const yPercent = (y / screenHeight) * 100;
|
||||
const widthPercent = (width / screenWidth) * 100;
|
||||
const heightPercent = (height / screenHeight) * 100;
|
||||
const contentId = $element.attr('data-content-id');
|
||||
const contentName = $element.attr('data-content-name');
|
||||
const contentType = $element.attr('data-content-type');
|
||||
const contentMetadata = $element.data('content-metadata');
|
||||
|
||||
const layer = {
|
||||
xPercent: xPercent,
|
||||
yPercent: yPercent,
|
||||
widthPercent: widthPercent,
|
||||
heightPercent: heightPercent,
|
||||
zIndex: parseInt($element.css('zIndex')),
|
||||
contentId: contentId ? parseInt(contentId) : null,
|
||||
contentName: contentName ? contentName : null,
|
||||
contentType: contentType ? contentType : null,
|
||||
contentMetadata: contentMetadata && contentMetadata !== "null" ? contentMetadata : null,
|
||||
};
|
||||
|
||||
layers.push(layer);
|
||||
});
|
||||
|
||||
layers.sort(function(a, b) {
|
||||
return parseInt(b.zIndex) - parseInt(a.zIndex);
|
||||
});
|
||||
|
||||
return {
|
||||
ratio: $('#elem-screen-ratio').val(),
|
||||
layers: layers
|
||||
};
|
||||
};
|
||||
});
|
||||
79
data/www/js/slideshow/content-text.js
Normal file
79
data/www/js/slideshow/content-text.js
Normal file
@ -0,0 +1,79 @@
|
||||
|
||||
jQuery(document).ready(function ($) {
|
||||
const contentData = JSON.parse($('#content-edit-location').val() || '{}');
|
||||
const screenRatio = 16/9;
|
||||
|
||||
$('.screen-holder').css({
|
||||
'padding-top': ( 1/ ( screenRatio ) * 100) + '%'
|
||||
});
|
||||
|
||||
$('.ratio-value').val(screenRatio);
|
||||
|
||||
$('#screen').css({
|
||||
width: $('#screen').width(),
|
||||
height: $('#screen').height(),
|
||||
position: 'relative',
|
||||
}).parents('.screen-holder:eq(0)').css({
|
||||
width: 'auto',
|
||||
'padding-top': '0px'
|
||||
});
|
||||
|
||||
const draw = function() {
|
||||
const $screen = $('#screen');
|
||||
const $text = $('<div class="text">');
|
||||
let insideText = $('#elem-text').val();
|
||||
|
||||
if ($('#elem-scroll-enable').is(':checked')) {
|
||||
const $wrapper = $('<marquee>');
|
||||
$wrapper.attr({
|
||||
scrollamount: $('#elem-scroll-speed').val(),
|
||||
direction: $('[name=scrollDirection]:checked').val(),
|
||||
behavior: 'scroll',
|
||||
loop: -1
|
||||
});
|
||||
$wrapper.append(insideText);
|
||||
insideText = $wrapper;
|
||||
}
|
||||
|
||||
$text.append(insideText);
|
||||
|
||||
let justifyContent = 'center';
|
||||
switch($('[name=textAlign]:checked').val()) {
|
||||
case 'left': justifyContent = 'flex-start'; break;
|
||||
case 'right': justifyContent = 'flex-end'; break;
|
||||
}
|
||||
|
||||
$text.css({
|
||||
padding: $('#elem-container-margin').val() + 'px',
|
||||
color: $('#elem-fg-color').val(),
|
||||
textAlign: $('[name=textAlign]:checked').val(),
|
||||
textDecoration: $('#elem-text-underline').is(':checked') ? 'underline' : 'normal',
|
||||
fontSize: $('#elem-font-size').val() + 'px',
|
||||
fontWeight: $('#elem-font-bold').is(':checked') ? 'bold' : 'normal',
|
||||
fontStyle: $('#elem-font-italic').is(':checked') ? 'italic' : 'normal',
|
||||
fontFamily: $('#elem-font-family').val() + ", 'Arial', 'sans-serif'",
|
||||
whiteSpace: $('#elem-single-line').is(':checked') ? 'nowrap' : 'normal',
|
||||
justifyContent: justifyContent
|
||||
});
|
||||
|
||||
$screen.css({
|
||||
backgroundColor: $('#elem-bg-color').val(),
|
||||
});
|
||||
|
||||
$screen.html($text);
|
||||
};
|
||||
|
||||
$(document).on('input', '#elementForm input, #elementForm select', function () {
|
||||
draw();
|
||||
});
|
||||
|
||||
draw();
|
||||
|
||||
|
||||
$(document).on('submit', 'form.form', function (e) {
|
||||
const location = $('form#elementForm').serializeObject();
|
||||
$('#content-edit-location').val(JSON.stringify(location));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -9,22 +9,23 @@ jQuery(document).ready(function ($) {
|
||||
}).data('input');
|
||||
|
||||
$form.find('.content-object-input').each(function() {
|
||||
const active = $(this).attr('data-input-type') === inputType;
|
||||
|
||||
if ($(this).is('input[type=file]')) {
|
||||
$(this).prop('disabled', !active).prop('required', active);
|
||||
$(this).parents('label:eq(0)').toggleClass('hidden', !active);
|
||||
} else {
|
||||
$(this).prop('disabled', !active).prop('required', active).toggleClass('hidden', !active);
|
||||
}
|
||||
const $input = $(this);
|
||||
const active = $input.attr('data-input-type') === inputType;
|
||||
const $holder = $input.parents('.from-group-condition:eq(0)');
|
||||
$holder.find('input, select, textarea').prop('disabled', !active).prop('required', active).toggleClass('hidden', !active);
|
||||
$holder.toggleClass('hidden', !active);
|
||||
});
|
||||
|
||||
const optionAttributes = $selectedOption.get(0).attributes;
|
||||
const color = optionAttributes['data-color'].value;
|
||||
$form.find('.object-label').html(optionAttributes['data-object-label'].value);
|
||||
$form.find('.object-label:visible').html(optionAttributes['data-object-label'].value);
|
||||
$('.type-icon').attr('class', 'type-icon fa ' + optionAttributes['data-icon'].value);
|
||||
$('.tab-select .widget').attr('class', 'widget ' + ('border-' + color) + ' ' + color);
|
||||
$form.find('button[type=submit]').attr('class', 'btn ' + ('btn-' + color));
|
||||
$form.find('button[type=submit]').attr('class', [
|
||||
'btn',
|
||||
`btn-${color}`,
|
||||
classColorXor(color, '')
|
||||
].join(' '));
|
||||
};
|
||||
|
||||
const main = function () {
|
||||
|
||||
@ -1,27 +1,69 @@
|
||||
jQuery(document).ready(function ($) {
|
||||
const loadDateTimePicker = function ($els) {
|
||||
const d = new Date();
|
||||
|
||||
$els.each(function () {
|
||||
var $el = $(this);
|
||||
$el.flatpickr({
|
||||
const $el = $(this);
|
||||
const timeOnly = isScheduleInWeekMoment($el);
|
||||
|
||||
const options = {
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
allowInput: false,
|
||||
noCalendar: false,
|
||||
allowInvalidPreload: false,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
dateFormat: timeOnly ? 'H:i' : 'Y-m-d H:i',
|
||||
defaultHour: d.getHours(),
|
||||
defaultMinute: d.getMinutes(),
|
||||
onChange: function (selectedDates, dateStr, instance) {
|
||||
const d = selectedDates[0];
|
||||
const $target = $el.parents('.widget:eq(0)').find('.target');
|
||||
$target.val(
|
||||
d ? `${d.getMinutes()} ${d.getHours()} ${d.getDate()} ${(d.getMonth() + 1)} * ${d.getFullYear()}` : ''
|
||||
);
|
||||
callScheduleChange($el);
|
||||
}
|
||||
});
|
||||
$el.addClass('hidden');
|
||||
})
|
||||
};
|
||||
|
||||
if (timeOnly) {
|
||||
options['noCalendar'] = true;
|
||||
|
||||
if ($el.val() === '') {
|
||||
$el.val('00:00');
|
||||
}
|
||||
} else {
|
||||
if ($el.val().indexOf('-') < 0) {
|
||||
$el.val('');
|
||||
}
|
||||
|
||||
if ($el.val() === '') {
|
||||
$el.val(prettyTimestamp(d.getTime()));
|
||||
}
|
||||
}
|
||||
|
||||
$el.flatpickr(options);
|
||||
});
|
||||
};
|
||||
|
||||
const onInDateTimeMomentChanged = function($el) {
|
||||
const $holder = $el.parents('.widget:eq(0)');
|
||||
const $datetimepicker = $holder.find('.datetimepicker');
|
||||
const $cronTarget = $holder.find('.target');
|
||||
const matches = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2}) (?<hour>\d{2}):(?<minute>\d{2})/.exec($datetimepicker.val());
|
||||
if (matches) {
|
||||
const {year, month, day, hour, minute} = matches.groups;
|
||||
$cronTarget.val(`${parseInt(minute)} ${parseInt(hour)} ${parseInt(day)} ${parseInt(month)} * ${parseInt(year)}`);
|
||||
} else {
|
||||
$cronTarget.val('');
|
||||
}
|
||||
};
|
||||
|
||||
const onInWeekMomentChanged = function($el) {
|
||||
const $holder = $el.parents('.widget:eq(0)');
|
||||
const $datetimepicker = $holder.find('.datetimepicker');
|
||||
const $weekdaypicker = $holder.find('.weekdaypicker');
|
||||
const $cronTarget = $holder.find('.target');
|
||||
const matches = $datetimepicker.val().split(':').map(function(e) { return parseInt(e) });
|
||||
if (matches.length === 2) {
|
||||
[hour, minute] = matches;
|
||||
$cronTarget.val(`${minute} ${hour} * * ${$weekdaypicker.val()}`);
|
||||
} else {
|
||||
$cronTarget.val('');
|
||||
}
|
||||
};
|
||||
|
||||
const getId = function ($el) {
|
||||
@ -36,28 +78,34 @@ jQuery(document).ready(function ($) {
|
||||
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/slideshow/slide/position',
|
||||
url: route_slide_position,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
data: JSON.stringify(positions),
|
||||
});
|
||||
};
|
||||
|
||||
const inputTypeUpdate = function () {
|
||||
const $modal = $('.modal-slide:visible');
|
||||
const $el = $('#slide-add-type');
|
||||
const value = $el.val();
|
||||
const inputType = $el.find('option').filter(function (i, el) {
|
||||
return $(el).val() === value;
|
||||
}).data('input');
|
||||
const getScheduleValue = function ($el) {
|
||||
const $scheduleGroup = $el.parents('.form-group:eq(0)');
|
||||
const $cronTrigger = $scheduleGroup.find('.trigger');
|
||||
return $cronTrigger.val();
|
||||
};
|
||||
|
||||
if ($modal.find('.picker:visible').length === 0) {
|
||||
$('.slide-add-object-input')
|
||||
.addClass('hidden')
|
||||
.prop('disabled', true).prop('required', false)
|
||||
.filter('#slide-add-object-input-' + inputType)
|
||||
.removeClass('hidden')
|
||||
.prop('disabled', false).prop('required', true)
|
||||
;
|
||||
const isScheduleInDateTimeMoment = function($el) {
|
||||
const scheduleValue = getScheduleValue($el);
|
||||
return scheduleValue === 'datetime';
|
||||
};
|
||||
|
||||
const isScheduleInWeekMoment = function($el) {
|
||||
const scheduleValue = getScheduleValue($el);
|
||||
return scheduleValue === 'inweek';
|
||||
};
|
||||
|
||||
const callScheduleChange = function($el) {
|
||||
if (isScheduleInWeekMoment($el)) {
|
||||
onInWeekMomentChanged($el);
|
||||
}
|
||||
if (isScheduleInDateTimeMoment($el)) {
|
||||
onInDateTimeMomentChanged($el);
|
||||
}
|
||||
};
|
||||
|
||||
@ -66,77 +114,66 @@ jQuery(document).ready(function ($) {
|
||||
const $scheduleStartGroup = $modal.find('.slide-schedule-group');
|
||||
const $scheduleEndGroup = $modal.find('.slide-schedule-end-group');
|
||||
const $durationGroup = $modal.find('.slide-duration-group');
|
||||
const $isNotificationGroup = $modal.find('.slide-notification-group');
|
||||
const $delegateDurationGroup = $modal.find('.slide-delegate-duration-group');
|
||||
const $contentGroup = $modal.find('.slide-content-id-group');
|
||||
|
||||
const $triggerStart = $scheduleStartGroup.find('.trigger');
|
||||
const $triggerEnd = $scheduleEndGroup.find('.trigger');
|
||||
const $targetCronFieldStart = $scheduleStartGroup.find('.target');
|
||||
const $targetCronFieldEnd = $scheduleEndGroup.find('.target');
|
||||
const $targetDuration = $durationGroup.find('input');
|
||||
const $targetDelegateDuration = $delegateDurationGroup.find('input')
|
||||
|
||||
const $datetimepickerStart = $scheduleStartGroup.find('.datetimepicker');
|
||||
const $datetimepickerEnd = $scheduleEndGroup.find('.datetimepicker');
|
||||
const $isNotification = $isNotificationGroup.find('.trigger');
|
||||
|
||||
const isNotification = $isNotification.prop('checked');
|
||||
const $weekdaypickerStart = $scheduleStartGroup.find('.weekdaypicker');
|
||||
const $weekdaypickerEnd = $scheduleEndGroup.find('.weekdaypicker');
|
||||
const $isNotification = $modal.find('.slide-is-notification');
|
||||
|
||||
const isNotification = $isNotification.val() === '1';
|
||||
const isVideo = $contentGroup.find('.target').attr('data-type') === 'video';
|
||||
let isLoopStart = $triggerStart.val() === 'loop';
|
||||
let isCronStart = $triggerStart.val() === 'cron';
|
||||
|
||||
function updateScheduleChoices(isNotification, isLoopStart, isCronStart) {
|
||||
let scheduleStartChoices = $.extend({}, schedule_start_choices);
|
||||
let scheduleEndChoices = $.extend({}, schedule_end_choices);
|
||||
|
||||
if (!isNotification || isLoopStart) {
|
||||
delete scheduleStartChoices['cron'];
|
||||
delete scheduleEndChoices['duration'];
|
||||
}
|
||||
|
||||
if (isNotification) {
|
||||
delete scheduleStartChoices['loop'];
|
||||
delete scheduleEndChoices['stayloop'];
|
||||
|
||||
if (isCronStart) {
|
||||
delete scheduleEndChoices['datetime'];
|
||||
}
|
||||
}
|
||||
|
||||
return {scheduleStartChoices, scheduleEndChoices};
|
||||
}
|
||||
|
||||
function applyChoices() {
|
||||
const {
|
||||
scheduleStartChoices,
|
||||
scheduleEndChoices
|
||||
} = updateScheduleChoices(isNotification, isLoopStart, isCronStart);
|
||||
recreateSelectOptions($triggerStart, scheduleStartChoices);
|
||||
recreateSelectOptions($triggerEnd, scheduleEndChoices);
|
||||
}
|
||||
|
||||
applyChoices();
|
||||
const choice_map = choices_map[isNotification ? 'notification' : 'normal']
|
||||
recreateSelectOptions($triggerStart, Object.keys(choice_map).reduce((obj, key) => {
|
||||
obj[key] = choices_translations[key];
|
||||
return obj;
|
||||
}, {}));
|
||||
recreateSelectOptions($triggerEnd, choice_map[$triggerStart.val()]);
|
||||
|
||||
isLoopStart = $triggerStart.val() === 'loop';
|
||||
isCronStart = $triggerStart.val() === 'cron';
|
||||
|
||||
const isCronEnd = $triggerEnd.val() === 'cron';
|
||||
const isInWeekMomentStart = $triggerStart.val() === 'inweek';
|
||||
const isDatetimeStart = $triggerStart.val() === 'datetime';
|
||||
const isDatetimeEnd = $triggerEnd.val() === 'datetime';
|
||||
const isStayloopEnd = $triggerEnd.val() === 'stayloop';
|
||||
const isDurationEnd = $triggerEnd.val() === 'duration';
|
||||
const isInWeekMomentEnd = $triggerEnd.val() === 'inweek';
|
||||
|
||||
const flushValueStart = isLoopStart;
|
||||
const flushValueEnd = isLoopStart || isStayloopEnd || isDurationEnd;
|
||||
const flushDuration = isNotification && isDatetimeEnd;
|
||||
const delegateDuration = $targetDelegateDuration.prop('checked');
|
||||
|
||||
function toggleVisibility() {
|
||||
$targetCronFieldStart.toggleClass('hidden', !isCronStart);
|
||||
$targetCronFieldEnd.toggleClass('hidden', !isCronEnd);
|
||||
$datetimepickerStart.toggleClass('hidden', !isDatetimeStart);
|
||||
$datetimepickerEnd.toggleClass('hidden', !isDatetimeEnd);
|
||||
$datetimepickerStart.toggleClass('hidden', !(isDatetimeStart || isInWeekMomentStart));
|
||||
$datetimepickerEnd.toggleClass('hidden', !isDatetimeEnd && !isInWeekMomentEnd);
|
||||
|
||||
$durationGroup.toggleClass('hidden', isNotification && isDatetimeEnd);
|
||||
$weekdaypickerStart.toggleClass('hidden', !isInWeekMomentStart);
|
||||
$weekdaypickerEnd.toggleClass('hidden', !isInWeekMomentEnd);
|
||||
|
||||
$delegateDurationGroup.toggleClass('hidden', (isNotification && isDatetimeEnd) || !isVideo);
|
||||
$durationGroup.toggleClass('hidden', (isNotification && isDatetimeEnd) || delegateDuration);
|
||||
|
||||
$targetDuration.prop('required', $durationGroup.is(':visible'));
|
||||
$targetDelegateDuration.prop('disabled', !$delegateDurationGroup.is(':visible'));
|
||||
$scheduleEndGroup.toggleClass('hidden', isLoopStart);
|
||||
|
||||
$durationGroup.find('.widget input').prop('required', $durationGroup.is(':visible'));
|
||||
}
|
||||
|
||||
function flushValues() {
|
||||
@ -155,11 +192,15 @@ jQuery(document).ready(function ($) {
|
||||
}
|
||||
}
|
||||
|
||||
loadDateTimePicker($modal.find('.datetimepicker'));
|
||||
toggleVisibility();
|
||||
flushValues();
|
||||
applyChoices();
|
||||
};
|
||||
|
||||
callScheduleChange($weekdaypickerStart);
|
||||
callScheduleChange($weekdaypickerEnd);
|
||||
callScheduleChange($datetimepickerStart);
|
||||
callScheduleChange($weekdaypickerEnd);
|
||||
};
|
||||
|
||||
const main = function () {
|
||||
$("ul.slides").sortable({
|
||||
@ -168,36 +209,26 @@ jQuery(document).ready(function ($) {
|
||||
});
|
||||
};
|
||||
|
||||
$(document).on('change', '.weekdaypicker', function() {
|
||||
callScheduleChange($(this));
|
||||
});
|
||||
|
||||
$(document).on('change', '.modal-slide select.trigger, .modal-slide input.trigger', function () {
|
||||
inputSchedulerUpdate();
|
||||
});
|
||||
|
||||
$(document).on('change', '#slide-add-type', inputTypeUpdate);
|
||||
|
||||
// $(document).on('click', '.picker button', function () {
|
||||
// const $parent = $(this).parents('.modal-slide-add');
|
||||
// $parent.find('.picker').addClass('hidden').find('select').prop('disabled', true);
|
||||
// $parent.find('.upload').removeClass('hidden').find('input,select').prop('disabled', false);
|
||||
// inputTypeUpdate();
|
||||
// });
|
||||
|
||||
$(document).on('click', '.slide-add', function () {
|
||||
showModal('modal-slide-add');
|
||||
const $modal = $('.modal-slide-add:visible');
|
||||
loadDateTimePicker($modal.find('.datetimepicker'));
|
||||
// $modal.find('.picker').removeClass('hidden').find('select').prop('disabled', false);
|
||||
// $modal.find('.upload').addClass('hidden').find('input,select').prop('disabled', true);
|
||||
// $modal.find('button[type=submit]').removeClass('hidden');
|
||||
// $modal.find('.btn-loading').addClass('hidden');
|
||||
inputTypeUpdate();
|
||||
showModal($(this).attr('data-modal'));
|
||||
const $modal = $('.modal-slide:visible');
|
||||
inputSchedulerUpdate();
|
||||
inputContentUpdate();
|
||||
$('.modal-slide-add input:eq(0)').focus().select();
|
||||
$modal.find('input[type=text]:visible:eq(0)').focus().select();
|
||||
});
|
||||
|
||||
$(document).on('click', '.content-explr-picker', function () {
|
||||
showPickers('modal-content-explr-picker', function (content) {
|
||||
inputContentUpdate(content)
|
||||
inputContentUpdate(content);
|
||||
inputSchedulerUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
@ -207,58 +238,84 @@ jQuery(document).ready(function ($) {
|
||||
const $inputLabel = $group.find('.target-label');
|
||||
const $inputId = $group.find('.target');
|
||||
const $actionShow = $group.find('.slide-content-show');
|
||||
const invalidContent = content === undefined || !content.id;
|
||||
|
||||
if (content === undefined || !content.id) {
|
||||
if (invalidContent) {
|
||||
$inputLabel.val('');
|
||||
$inputId.val('');
|
||||
$actionShow.addClass('hidden');
|
||||
$inputId.attr('data-type', '');
|
||||
return;
|
||||
}
|
||||
|
||||
$inputLabel.val(content.name);
|
||||
$inputId.val(content.id);
|
||||
$inputId.attr('data-type', content.type);
|
||||
$actionShow.removeClass('hidden');
|
||||
};
|
||||
|
||||
$(document).on('change', '.slide-delegate-duration', function () {
|
||||
inputSchedulerUpdate();
|
||||
});
|
||||
|
||||
$(document).on('click', '.slide-content-show', function () {
|
||||
window.open($(this).attr('data-route').replace('__id__', $(this).parents('.widget:eq(0)').find('.target').val()));
|
||||
});
|
||||
|
||||
$(document).on('click', '.slide-edit', function () {
|
||||
const slide = JSON.parse($(this).parents('.slide-item:eq(0)').attr('data-entity'));
|
||||
showModal('modal-slide-edit');
|
||||
showModal($(this).attr('data-modal'));
|
||||
const $modal = $('.modal-slide:visible');
|
||||
|
||||
const hasCron = slide.cron_schedule && slide.cron_schedule.length > 0;
|
||||
const hasDateTime = hasCron && validateCronDateTime(slide.cron_schedule);
|
||||
const isInDateTimeMomentStart = hasCron && isCronInDatetimeMoment(slide.cron_schedule);
|
||||
const isInWeekMomentStart = hasCron && isCronInWeekMoment(slide.cron_schedule);
|
||||
|
||||
const hasCronEnd = slide.cron_schedule_end && slide.cron_schedule_end.length > 0;
|
||||
const hasDateTimeEnd = hasCronEnd && validateCronDateTime(slide.cron_schedule_end);
|
||||
const isInDateTimeMomentEnd = hasCronEnd && isCronInDatetimeMoment(slide.cron_schedule_end);
|
||||
const isInWeekMomentEnd = hasCronEnd && isCronInWeekMoment(slide.cron_schedule_end);
|
||||
const isNotification = slide.is_notification;
|
||||
|
||||
inputContentUpdate(slide.content);
|
||||
const tclass = '#slide-' + (isNotification ? 'notification-' : '') + 'edit';
|
||||
|
||||
$('.modal-slide-edit input:visible:eq(0)').focus().select();
|
||||
$('#slide-edit-duration').val(slide.duration);
|
||||
$('#slide-edit-is-notification').prop('checked', isNotification);
|
||||
$('#slide-edit-enabled').prop('checked', slide.enabled);
|
||||
const inputCallbacks = function() {
|
||||
inputContentUpdate(slide.content);
|
||||
inputSchedulerUpdate();
|
||||
};
|
||||
|
||||
$('#slide-edit-cron-schedule').val(slide.cron_schedule).toggleClass('hidden', !hasCron || hasDateTime);
|
||||
$('#slide-edit-cron-schedule-trigger').val(hasDateTime ? 'datetime' : (hasCron ? 'cron' : 'loop'));
|
||||
inputCallbacks();
|
||||
|
||||
$('#slide-edit-cron-schedule-end').val(slide.cron_schedule_end).toggleClass('hidden', !hasCronEnd || hasDateTimeEnd);
|
||||
$('#slide-edit-cron-schedule-end-trigger').val(hasDateTimeEnd ? 'datetime' : (hasCronEnd ? 'cron' : (isNotification ? 'duration' : 'stayloop')));
|
||||
$modal.find(tclass + '-delegate-duration').prop('checked', slide.delegate_duration);
|
||||
|
||||
$('#slide-edit-cron-schedule-datetimepicker').toggleClass('hidden', !hasDateTime).val(
|
||||
hasDateTime ? getCronDateTime(slide.cron_schedule) : ''
|
||||
$modal.find('input[type=text]:visible:eq(0)').focus().select();
|
||||
$modal.find(tclass + '-duration').val(slide.duration);
|
||||
$modal.find(tclass + '-enabled').prop('checked', slide.enabled);
|
||||
|
||||
$modal.find(tclass + '-cron-schedule').val(slide.cron_schedule).toggleClass('hidden', !hasCron || isInDateTimeMomentStart || isInWeekMomentStart);
|
||||
$modal.find(tclass + '-cron-schedule-trigger').val(isInWeekMomentStart ? 'inweek' : (isInDateTimeMomentStart ? 'datetime' : (hasCron ? 'cron' : 'loop')));
|
||||
|
||||
inputCallbacks();
|
||||
|
||||
$modal.find(tclass + '-cron-schedule-end').val(slide.cron_schedule_end).toggleClass('hidden', !hasCronEnd || isInDateTimeMomentEnd || isInWeekMomentEnd);
|
||||
$modal.find(tclass + '-cron-schedule-end-trigger').val(isInWeekMomentEnd ? 'inweek' : (isInDateTimeMomentEnd ? 'datetime' : (hasCronEnd ? 'cron' : (isNotification ? 'duration' : 'stayloop'))));
|
||||
|
||||
$modal.find(tclass + '-cron-schedule-datetimepicker').toggleClass('hidden', !(isInDateTimeMomentStart || isInWeekMomentStart)).val(
|
||||
isInWeekMomentStart ? getCronTime(slide.cron_schedule) : (isInDateTimeMomentStart ? getCronDateTime(slide.cron_schedule) : '')
|
||||
);
|
||||
$modal.find(tclass + '-cron-schedule-weekdaypicker').toggleClass('hidden', !isInWeekMomentStart).val(
|
||||
isInWeekMomentStart ? getCronDayInWeek(slide.cron_schedule) : '1'
|
||||
);
|
||||
|
||||
$('#slide-edit-cron-schedule-end-datetimepicker').toggleClass('hidden', !hasDateTimeEnd).val(
|
||||
hasDateTimeEnd ? getCronDateTime(slide.cron_schedule_end) : ''
|
||||
$modal.find(tclass + '-cron-schedule-end-datetimepicker').toggleClass('hidden', !(isInDateTimeMomentEnd || isInWeekMomentEnd)).val(
|
||||
isInWeekMomentEnd ? getCronTime(slide.cron_schedule_end) : (isInDateTimeMomentEnd ? getCronDateTime(slide.cron_schedule_end) : '')
|
||||
);
|
||||
$modal.find(tclass + '-cron-schedule-end-weekdaypicker').toggleClass('hidden', !isInWeekMomentEnd).val(
|
||||
isInWeekMomentEnd ? getCronDayInWeek(slide.cron_schedule_end) : '1'
|
||||
);
|
||||
$('#slide-edit-id').val(slide.id);
|
||||
loadDateTimePicker($('.modal-slide-edit .datetimepicker'));
|
||||
|
||||
inputSchedulerUpdate();
|
||||
$modal.find(tclass + '-id').val(slide.id);
|
||||
inputCallbacks();
|
||||
loadDateTimePicker($modal.find('.datetimepicker'));
|
||||
});
|
||||
|
||||
$(document).on('click', '.slide-delete', function () {
|
||||
@ -280,7 +337,7 @@ jQuery(document).ready(function ($) {
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('submit', '.modal-slide-add form', function () {
|
||||
$(document).on('submit', '.modal-slide form', function () {
|
||||
$(this).find('button[type=submit]').addClass('hidden');
|
||||
$(this).find('.btn-loading').removeClass('hidden');
|
||||
});
|
||||
|
||||
@ -31,13 +31,20 @@ jQuery(function ($) {
|
||||
},
|
||||
always: function (e, data) {
|
||||
const response = data._response.jqXHR;
|
||||
let statusCode = response.status;
|
||||
$button.removeClass('uploading').removeClass('btn-naked btn-super-upload-busy').addClass('btn-info btn-super-upload');
|
||||
if (response.status != 200) {
|
||||
const $alert = $('.alert-danger').removeClass('hidden');
|
||||
if (response.status == 413) {
|
||||
$alert.text(l.js_common_http_error_413);
|
||||
|
||||
let errorComment = response.responseText.match(/<!--\s*error=(\d+);\s*-->/);
|
||||
if (errorComment && errorComment[1]) {
|
||||
statusCode = parseInt(errorComment[1], 10);
|
||||
}
|
||||
|
||||
if (statusCode !== 200) {
|
||||
const $alert = $('.alert-upload').removeClass('hidden');
|
||||
if (statusCode === 413) {
|
||||
$alert.html(`<i class="fa fa-warning"></i>${l.js_common_http_error_413}`);
|
||||
} else {
|
||||
$alert.text(l.js_common_http_error_occured.replace('%code%', response.status));
|
||||
$alert.html(`<i class="fa fa-warning"></i>${l.js_common_http_error_occured.replace('%code%', statusCode)}`);
|
||||
}
|
||||
} else {
|
||||
document.location.reload();
|
||||
@ -8,13 +8,28 @@ const getCronDateTime = function(cronExpression) {
|
||||
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')} ${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const validateCronDateTime = function(cronExpression) {
|
||||
const getCronTime = function(cronExpression) {
|
||||
const [minutes, hours, day, month, _, year] = cronExpression.split(' ');
|
||||
return `${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const getCronDayInWeek = function(cronExpression) {
|
||||
const [minutes, hours, day, month, day_week, year] = cronExpression.split(' ');
|
||||
return day_week;
|
||||
};
|
||||
|
||||
const isCronInDatetimeMoment = function(cronExpression) {
|
||||
const pattern = /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\*\s+(\d+)$/;
|
||||
return pattern.test(cronExpression);
|
||||
};
|
||||
|
||||
const isCronInWeekMoment = function(cronExpression) {
|
||||
const pattern = /^(\d+)\s+(\d+)\s+\*\s+\*\s+(\d+)$/;
|
||||
return pattern.test(cronExpression);
|
||||
};
|
||||
|
||||
const cronToDateTimeObject = function(cronExpression) {
|
||||
if (!validateCronDateTime(cronExpression)) {
|
||||
if (!isCronInDatetimeMoment(cronExpression)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -66,3 +81,9 @@ const secondsToHHMMSS = function (seconds) {
|
||||
const secs = seconds % 60;
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const evalStringRatio = function(str) {
|
||||
return str.replace(/(\d+)\/(\d+)/g, function(match, p1, p2) {
|
||||
return (parseInt(p1) / parseInt(p2)).toString();
|
||||
});
|
||||
};
|
||||
|
||||
0
data/www/plugins/.gitkeep
Executable file
0
data/www/plugins/.gitkeep
Executable file
@ -31,8 +31,16 @@ main {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.contex-tail {
|
||||
margin-right: 20px;
|
||||
.context-tail {
|
||||
margin-right: 30px;
|
||||
|
||||
.btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.context-tail-auth {
|
||||
margin-right: 10px;
|
||||
|
||||
.btn {
|
||||
margin-right: 0;
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: $gscale1;
|
||||
background-color: $layoutBackground;
|
||||
}
|
||||
|
||||
body, html {
|
||||
@ -26,6 +26,10 @@ body, html {
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
&.fx-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical {
|
||||
@ -96,9 +100,7 @@ main {
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: radial-gradient(circle at 0% 53%, rgba($pinkyRed, 0.8) 10%, transparent 45%),
|
||||
radial-gradient(circle at 135% 53%, rgba($seaBlue, 0.8) 10%, transparent 95%),
|
||||
radial-gradient(circle at 50% 80%, rgba($limeGreen, 0.8) 40%, transparent 95%);
|
||||
background: $fancyBackground;
|
||||
|
||||
.page-content {
|
||||
flex: 2;
|
||||
@ -117,9 +119,24 @@ main {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding: 0 10px 40px 10px;
|
||||
background: $gscale1;
|
||||
padding: 10px 10px 40px 10px;
|
||||
background: $layoutBackground;
|
||||
align-self: stretch;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $gscaleD;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
color: $gscale6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,7 +144,7 @@ main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
align-self: stretch;
|
||||
background: $gscale1;
|
||||
background: $layoutBackground;
|
||||
border-top: none;
|
||||
|
||||
&.left-panel {
|
||||
@ -138,7 +155,7 @@ main {
|
||||
flex: 0.5;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
background: $gscale1;
|
||||
background: $layoutBackground;
|
||||
box-shadow: 1px 1px .5px .5px inset rgba($gscale0, 0.2);
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
menu {
|
||||
width: 300px;
|
||||
background: $gscale1;
|
||||
background: $layoutBackground;
|
||||
overflow-y: auto;
|
||||
overflow-x: visible;
|
||||
padding: 20px;
|
||||
|
||||
@ -7,30 +7,15 @@
|
||||
align-items: center;
|
||||
border-radius: $baseRadius;
|
||||
|
||||
i {
|
||||
margin-right: 13px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: $info;
|
||||
background: rgba($info, .2);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: $success;
|
||||
background: rgba($success, .2);
|
||||
}
|
||||
|
||||
.alert-danger,
|
||||
.alert-error {
|
||||
color: $danger;
|
||||
background: rgba($danger, .2);
|
||||
}
|
||||
|
||||
.alert i {
|
||||
margin-right: 13px;
|
||||
}
|
||||
12
data/www/scss/components/_animation.scss
Normal file
12
data/www/scss/components/_animation.scss
Normal file
@ -0,0 +1,12 @@
|
||||
@keyframes blink{50%{opacity:0;}}
|
||||
.cfx-blink{animation:1.5s linear infinite blink;}
|
||||
.cfx-ffff-speed {animation-delay: 0.1s;}
|
||||
.cfx-fff-speed {animation-delay: 0.3s;}
|
||||
.cfx-ff-speed {animation-delay: 0.5s;}
|
||||
.cfx-f-speed {animation-delay: 0.8s;}
|
||||
.cfx-m-speed {animation-delay: 1s;}
|
||||
.cfx-s-speed {animation-delay: 1.3s;}
|
||||
.cfx-ss-speed {animation-delay: 1.5s;}
|
||||
.cfx-sss-speed {animation-delay: 1.8s;}
|
||||
.cfx-ssss-speed {animation-delay: 2s;}
|
||||
.cfx-sssss-speed {animation-delay: 3s;}
|
||||
@ -1,32 +1,10 @@
|
||||
a.badge,
|
||||
.badge {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px 5px;
|
||||
border-radius: $baseRadius;
|
||||
font-size: 12px;
|
||||
background: rgba($gscaleF, .1);
|
||||
border: 1px solid transparent;
|
||||
color: $gscaleF;
|
||||
}
|
||||
|
||||
a.badge:hover {
|
||||
color: $gscaleF;
|
||||
border: 1px solid rgba($gscaleF, .4);
|
||||
}
|
||||
|
||||
.panel-inactive .badge {
|
||||
background: rgba($gscale7, .1);
|
||||
color: $gscale7;
|
||||
}
|
||||
|
||||
.panel-inactive a.badge:hover {
|
||||
color: $gscale7;
|
||||
border: 1px solid rgba($gscale7,.2);
|
||||
}
|
||||
|
||||
.badge.anonymous {
|
||||
opacity: .2;
|
||||
.badge-inset {
|
||||
display: inline;
|
||||
color: $gscaleA;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
background: $gscale0;
|
||||
border: 1px solid $gscale3;
|
||||
border-radius: $baseRadius;
|
||||
padding: 3px 7px;
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
|
||||
button,
|
||||
.btn {
|
||||
$shadowOffset: 2px;
|
||||
@ -56,6 +57,7 @@ button,
|
||||
box-shadow: 0 $shadowOffset 0 0 darken($gscale5, 10%);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
box-shadow: 0 $shadowOffset 0 1px $gkscale2 inset;
|
||||
background: darken($gscale5, 10%);
|
||||
@ -141,4 +143,3 @@ button,
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -190,13 +190,14 @@ ul.explr-dirview {
|
||||
max-width: 84px;
|
||||
min-width: 84px;
|
||||
position: relative;
|
||||
word-break: break-all;
|
||||
|
||||
&.with-thumbnail {
|
||||
|
||||
.img-holder {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: $gscale07;
|
||||
background: $black;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -295,6 +296,7 @@ ul.explr-dirview {
|
||||
|
||||
.explr-tree {
|
||||
width: 400px;
|
||||
max-width: 400px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
background: $gscale2;
|
||||
|
||||
@ -19,3 +19,17 @@
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blinkfade {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
.pickers,
|
||||
.modals {
|
||||
position: fixed;
|
||||
background: rgba($gscale0, 0.4);
|
||||
background: rgba($gkscale0, 0.4);
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
@ -34,13 +34,14 @@
|
||||
|
||||
.modals-outer {
|
||||
min-width: 464px;
|
||||
max-width: 464px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding-bottom: 2px;
|
||||
|
||||
.modals-inner {
|
||||
background: $gscale1;
|
||||
background: $layoutBackground;
|
||||
border-radius: 10px;
|
||||
color: lighten($gscale0, 20%);
|
||||
padding: 40px;
|
||||
@ -57,7 +58,7 @@
|
||||
|
||||
h3 {
|
||||
align-self: stretch;
|
||||
margin: 0;
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
margin: 1px 1px 28px 1px;
|
||||
background: $gscale1;
|
||||
background: $layoutBackground;
|
||||
border-radius: $baseRadius;
|
||||
border: 4px solid rgba($gscaleF, .05);
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: $gscale04;
|
||||
background-color: $gscale14;
|
||||
}
|
||||
|
||||
.pane-cell,
|
||||
@ -139,6 +139,30 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.highlighted:hover,
|
||||
&.highlighted {
|
||||
background-color: $seaBlue;
|
||||
|
||||
td {
|
||||
font-weight: bold;
|
||||
color: $gscaleF;
|
||||
|
||||
i.icon-legend {
|
||||
color: $gscaleF;
|
||||
}
|
||||
|
||||
span,
|
||||
i.icon-value {
|
||||
background-color: rgba($gscaleF, .3);
|
||||
color: $gscaleF;
|
||||
}
|
||||
|
||||
&.description {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -36,7 +36,7 @@
|
||||
&.active {
|
||||
border-left: 4px solid $seaBlue;
|
||||
border-radius: $baseRadius;
|
||||
border-bottom: 2px solid $gscale07;
|
||||
border-bottom: 2px solid $gscale17;
|
||||
background: $gscale2;
|
||||
color: $seaBlue;
|
||||
|
||||
@ -65,6 +65,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.starred {
|
||||
.tile-tail {
|
||||
.head-icon {
|
||||
i {
|
||||
font-size: 8px;
|
||||
color: $other;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.tile-body {
|
||||
opacity: .3;
|
||||
|
||||
@ -25,6 +25,14 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
.from-group-condition {
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -35,10 +43,10 @@ form {
|
||||
flex: 1;
|
||||
margin-bottom: 20px;
|
||||
|
||||
|
||||
label {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
@ -73,18 +81,48 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-group,
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0 5px 0 0 !important;
|
||||
justify-content: center !important;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.widget {
|
||||
margin-top: 10px;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.btn {
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
|
||||
select,
|
||||
input {
|
||||
align-self: stretch;
|
||||
|
||||
&:first-child {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input + .btn + .btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&.widget-unit {
|
||||
|
||||
select,
|
||||
input {
|
||||
flex-grow: 0;
|
||||
@ -110,6 +148,33 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
input {
|
||||
&.size-m {
|
||||
max-width: 122px;
|
||||
}
|
||||
|
||||
&.color-picker {
|
||||
max-width: 125px;
|
||||
}
|
||||
|
||||
&.chars-4 {
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
&.chars-3 {
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
&.chars-2 {
|
||||
max-width: 20px;
|
||||
}
|
||||
|
||||
&.chars-1 {
|
||||
max-width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
color: rgba($gscaleF, .7);
|
||||
font-size: 14px;
|
||||
@ -134,22 +199,17 @@ form {
|
||||
color: $gscale5;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid $gscale3;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&.input-naked {
|
||||
padding-left: 0;
|
||||
color: $gscaleB;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&[disabled] {
|
||||
border: none;
|
||||
background: $gscale0;
|
||||
border-radius: $baseRadius;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,19 +18,20 @@
|
||||
@import 'components/modals';
|
||||
@import 'components/toast';
|
||||
@import 'components/dragdrop';
|
||||
|
||||
// Legacy
|
||||
@import 'components/animation';
|
||||
@import 'components/panes';
|
||||
@import 'components/tiles';
|
||||
@import 'components/empty';
|
||||
@import 'components/switches';
|
||||
//@import 'components/badges';
|
||||
@import 'components/badges';
|
||||
|
||||
// Import form styles
|
||||
@import 'forms/forms';
|
||||
|
||||
// Import pages styles
|
||||
@import 'pages/content';
|
||||
@import 'pages/content-composition';
|
||||
@import 'pages/content-text';
|
||||
@import 'pages/logs';
|
||||
@import 'pages/node-player';
|
||||
@import 'pages/playlist';
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
$layoutBackground: $gscale1;
|
||||
$fancyBackground: radial-gradient(circle at 0% 53%, rgba($pinkyRed, 0.8) 10%, transparent 45%),
|
||||
radial-gradient(circle at 135% 53%, rgba($seaBlue, 0.8) 10%, transparent 95%),
|
||||
radial-gradient(circle at 50% 80%, rgba($limeGreen, 0.8) 40%, transparent 95%);
|
||||
|
||||
@ -1,2 +1,8 @@
|
||||
$limeGreen: $bitterGreen;
|
||||
$yellow: rgb(255, 167, 10);
|
||||
$layoutBackground: $white;
|
||||
$fancyBackground: radial-gradient(circle at 0% 53%, rgba($seaBlue, 0.8) 10%, transparent 45%),
|
||||
radial-gradient(circle at 135% 53%, rgba($seaBlue, 0.8) 10%, transparent 95%),
|
||||
radial-gradient(circle at 50% 80%, rgba($bitterPurple, 0.8) 40%, transparent 95%);
|
||||
|
||||
|
||||
|
||||
@ -7,27 +7,91 @@ menu:hover h1.logo a {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
ul.explr-dirview li a i {
|
||||
color: $seaBlue;
|
||||
}
|
||||
ul.explr-dirview li a {
|
||||
&.with-thumbnail {
|
||||
.img-holder {
|
||||
background: $gkscaleD;
|
||||
}
|
||||
}
|
||||
|
||||
button.btn-neutral:hover,
|
||||
.btn.btn-neutral:hover {
|
||||
box-shadow: 0 2px 0 1px $gkscale6 inset;
|
||||
}
|
||||
|
||||
button.btn-naked,
|
||||
.btn.btn-naked {
|
||||
color: $gkscale7;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 0 1px $gkscale6 inset;
|
||||
i {
|
||||
color: $seaBlue;
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
.btn {
|
||||
&.btn-pixel {
|
||||
background: $white;
|
||||
color: $gkscale4;
|
||||
|
||||
.tiles .tiles-inner .tile-item:hover, .tiles .tiles-inner .tile-item.active {
|
||||
background: $white;
|
||||
@include pixel-box(4, $gkscaleC);
|
||||
|
||||
&:hover {
|
||||
@include pixel-box(6, $gkscaleC);
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-naked {
|
||||
color: $gkscale7;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 0 1px $gkscale6 inset;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-neutral:hover {
|
||||
box-shadow: 0 2px 0 1px $gkscale6 inset;
|
||||
}
|
||||
|
||||
&.btn-neutral {
|
||||
$shadowOffset: 2;
|
||||
color: $gkscale5;
|
||||
background: $white;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
box-shadow: 0 $shadowOffset 0 1px $gkscale2 inset;
|
||||
background: $gkscaleC;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: darken($gscale5, 20%);
|
||||
border: 1px solid $gscaleA;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tiles .tiles-inner .tile-item {
|
||||
border-top: 2px solid transparent;
|
||||
border-right: 2px solid transparent;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border-left: 2px solid $gscale17;
|
||||
border-top: 2px solid $gscale17;
|
||||
border-right: 2px solid $gscale17;
|
||||
border-bottom: 2px solid $gscale17;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border-color: $seaBlue;
|
||||
}
|
||||
|
||||
&:hover.starred,
|
||||
&.active.starred {
|
||||
border-color: $yellow;
|
||||
}
|
||||
|
||||
&:hover.disabled,
|
||||
&.active.disabled {
|
||||
border-color: $gscale4;
|
||||
}
|
||||
}
|
||||
|
||||
.panes {
|
||||
@ -37,41 +101,44 @@ button.btn-naked,
|
||||
.pane-item,
|
||||
tr {
|
||||
&:nth-child(odd) {
|
||||
background-color: $gkscaleF4;
|
||||
background-color: $gkscaleF;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: $gscale1;
|
||||
background-color: $gkscaleF7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-group .widget select, .form-group .widget input, .form-group .widget textarea {
|
||||
box-shadow: 0 2px 1px $gkscaleA, 0 4px 2px $gkscaleA inset;
|
||||
box-shadow: 0 2px 1px $gkscaleD, 0 4px 2px $gkscaleD inset;
|
||||
color: $gkscale5;
|
||||
background: $gkscaleC;
|
||||
background: $gkscaleE;
|
||||
}
|
||||
|
||||
.toggle label {
|
||||
box-shadow: 0 2px 2px $gkscaleA inset;
|
||||
&::after {
|
||||
box-shadow: 0 2px rgba($gkscaleA, 0.9);
|
||||
.toggle {
|
||||
label {
|
||||
box-shadow: 0 2px 2px $gkscaleC inset;
|
||||
|
||||
&::after {
|
||||
box-shadow: 0 2px rgba($gkscaleC, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + label {
|
||||
box-shadow: 0 2px 2px rgba($gkscale0, .3) inset;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-explr-picker {
|
||||
.explr-tree {
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ul.pills {
|
||||
box-shadow: 1px 1px .5px .5px inset rgba($gkscaleA, 0.2);
|
||||
background: #EEE;
|
||||
|
||||
li {
|
||||
a {
|
||||
color: $gkscale4;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@ -85,6 +152,8 @@ ul.pills {
|
||||
|
||||
.breadcrumb-container ul.breadcrumb {
|
||||
box-shadow: 1px 1px .5px .5px inset rgba($gkscaleA, 0.2);
|
||||
background: #EEE;
|
||||
|
||||
li a,
|
||||
li span,
|
||||
li {
|
||||
@ -100,23 +169,14 @@ ul.pills {
|
||||
}
|
||||
}
|
||||
|
||||
.view-player-group-list main .main-container .bottom-content .page-content .inner .node-player-group-holder .preview,
|
||||
.view-playlist-list main .main-container .bottom-content .page-content .inner .playlist-holder .preview {
|
||||
border-color: $gkscaleC;
|
||||
}
|
||||
|
||||
.view-player-group-list main .main-container .players-holder ul.players li.player-item .body,
|
||||
.view-playlist-list main .main-container .slides-holder ul.slides li.slide-item .body {
|
||||
background: $gkscaleD;
|
||||
}
|
||||
|
||||
.dropdown ul.dropdown-menu li.danger:hover a {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.inner-empty i {
|
||||
color: $gkscaleB;
|
||||
text-shadow: 0 -1px $gkscale7, 0 0px .5px $gkscale4;
|
||||
color: $gkscaleD;
|
||||
text-shadow: 0 -1px $gkscale9, 0 0px .5px $gkscale6;
|
||||
}
|
||||
|
||||
.view-player-group-list main .main-container .players-holder ul.players li.player-item .tail a,
|
||||
@ -126,4 +186,31 @@ ul.pills {
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-group {
|
||||
.widget {
|
||||
select,
|
||||
input,
|
||||
textarea {
|
||||
|
||||
&.disabled,
|
||||
&[disabled] {
|
||||
border: none;
|
||||
background: $gkscaleE;
|
||||
border-radius: $baseRadius;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-explr-picker {
|
||||
.explr-tree {
|
||||
background: $gkscaleF7;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
364
data/www/scss/pages/_content-composition.scss
Normal file
364
data/www/scss/pages/_content-composition.scss
Normal file
@ -0,0 +1,364 @@
|
||||
.view-content-edit.view-content-edit-composition main .main-container {
|
||||
|
||||
.page-panel.left-panel {
|
||||
flex: 1;
|
||||
|
||||
.form-holder {
|
||||
margin: 20px 20px 20px 10px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.page-panel.right-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h3.main {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
margin-top: 5px;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h4 {
|
||||
margin-right: 5px;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button {
|
||||
padding: 3px 15px;
|
||||
margin:0 3px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
min-height: initial;
|
||||
border: 1px solid $gkscale3;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-holder {
|
||||
//display: flex;
|
||||
//flex-direction: row;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: 56.25%; /* 16:9 aspect ratio */
|
||||
overflow: hidden;
|
||||
border-radius: $baseRadius;
|
||||
outline: 4px solid rgba($gscaleF, .1);
|
||||
|
||||
.screen {
|
||||
background-color: #ddd;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
|
||||
.element {
|
||||
position: absolute !important;
|
||||
background-color: $gkscaleE;
|
||||
outline: 1px solid $gkscaleC;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.focused {
|
||||
border: none;
|
||||
outline: 2px solid $seaBlue;
|
||||
z-index: 89 !important;
|
||||
|
||||
.ui-resizable-handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
color: $gkscaleC;
|
||||
|
||||
&.fa-cog {
|
||||
text-shadow: 0 -2px $gkscaleB, 0 0px 2px $gkscaleB;
|
||||
}
|
||||
|
||||
&.gscaleF {
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate-handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: red;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -15px;
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.ui-resizable-handle {
|
||||
$size: 10px;
|
||||
$sizeOffset: -1*calc($size/2);
|
||||
background: $gkscaleA;
|
||||
border: 1px solid $gkscale5;
|
||||
width: $size;
|
||||
height: $size;
|
||||
z-index: 90;
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
&.ui-resizable-n {
|
||||
cursor: n-resize;
|
||||
top: $sizeOffset;
|
||||
left: 50%;
|
||||
margin-left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-s {
|
||||
cursor: s-resize;
|
||||
bottom: $sizeOffset;
|
||||
left: 50%;
|
||||
margin-left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-w {
|
||||
cursor: w-resize;
|
||||
left: $sizeOffset;
|
||||
top: 50%;
|
||||
margin-top: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-e {
|
||||
cursor: e-resize;
|
||||
right: $sizeOffset;
|
||||
top: 50%;
|
||||
margin-top: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-nw {
|
||||
cursor: nw-resize;
|
||||
top: $sizeOffset;
|
||||
left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-ne {
|
||||
cursor: ne-resize;
|
||||
top: $sizeOffset;
|
||||
right: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-sw {
|
||||
cursor: sw-resize;
|
||||
bottom: $sizeOffset;
|
||||
left: $sizeOffset;
|
||||
}
|
||||
|
||||
&.ui-resizable-se {
|
||||
cursor: se-resize;
|
||||
bottom: $sizeOffset;
|
||||
right: $sizeOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.elements-holder {
|
||||
align-self: stretch;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
margin: 0 0 20px 0;
|
||||
|
||||
&.divide {
|
||||
border-top: 1px solid $gscale2;
|
||||
margin-top: 10px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-elements-list {
|
||||
padding: 10px;
|
||||
background: $gscale2;
|
||||
border-radius: $baseRadius;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-self: flex-start;
|
||||
|
||||
.element-list-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
> i {
|
||||
color: $gscaleE;
|
||||
margin:0 10px 0 0;
|
||||
cursor: move;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inner:hover,
|
||||
&.focused .inner {
|
||||
background-color: $seaBlue;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
|
||||
button.btn-naked {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
cursor: pointer;
|
||||
padding: 5px 5px 5px 10px;
|
||||
margin-bottom: 5px;
|
||||
background: $gkscaleE;
|
||||
border-radius: $baseRadius;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
color: $gkscale2;
|
||||
min-height: 46px;
|
||||
flex: 1;
|
||||
|
||||
label {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 219px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
display: none;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
button.btn-naked {
|
||||
color: $gscale5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
label {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-element-properties {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.divide {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
|
||||
&[disabled] {
|
||||
padding: 8px 0 5px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, .05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
155
data/www/scss/pages/_content-text.scss
Normal file
155
data/www/scss/pages/_content-text.scss
Normal file
@ -0,0 +1,155 @@
|
||||
.view-content-edit.view-content-edit-text main .main-container {
|
||||
|
||||
.page-panel.left-panel {
|
||||
flex: 1;
|
||||
|
||||
.form-holder {
|
||||
margin: 20px 20px 20px 10px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.page-panel.right-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h3.main {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
margin-top: 5px;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.screen-holder {
|
||||
//display: flex;
|
||||
//flex-direction: row;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding-top: 56.25%; /* 16:9 aspect ratio */
|
||||
overflow: hidden;
|
||||
border-radius: $baseRadius;
|
||||
outline: 4px solid rgba($gscaleF, .1);
|
||||
background: repeating-conic-gradient(#EEE 0% 25%, white 0% 50%) 50% / 20px 20px;
|
||||
|
||||
.screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: flex;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
|
||||
marquee {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-element-properties {
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $gscaleD;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid $gscale2;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.divide {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
label {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
|
||||
&[disabled] {
|
||||
padding: 8px 0 5px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, .05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -23,6 +23,29 @@
|
||||
|
||||
.view-content-edit main .main-container {
|
||||
|
||||
.top-content {
|
||||
h3 {
|
||||
color: $gscaleF;
|
||||
padding: 10px 10px 10px 0;
|
||||
font-size: 16px;
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
|
||||
span {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: $baseRadius;
|
||||
padding: 4px 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-content {
|
||||
.page-content {
|
||||
flex: 1;
|
||||
@ -38,32 +61,11 @@
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
|
||||
h3 {
|
||||
color: $gscaleF;
|
||||
padding: 10px 10px 10px 0;
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
align-self: stretch;
|
||||
margin-left: -8px;
|
||||
|
||||
span {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: $baseRadius;
|
||||
padding: 4px 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -87,7 +89,3 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -100,7 +100,7 @@
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin: 0 10px 0 10px;
|
||||
background: $gscale0B;
|
||||
background: $gscale1B;
|
||||
padding: 10px;
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
|
||||
@ -75,12 +75,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.preview-holder {
|
||||
position: relative;
|
||||
|
||||
.form-group {
|
||||
flex-grow: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hover-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.hover-only {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
&:hover {
|
||||
background: $gkscaleC;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
|
||||
@ -11,25 +11,75 @@
|
||||
align-self: stretch;
|
||||
color: $gscale6;
|
||||
}
|
||||
|
||||
.modal-playlist-qrcode {
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrcode-pic {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
border: 4px solid $gscale5;
|
||||
border-radius: $baseRadius;
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
//.modal-playlist-cast-scan {
|
||||
// h2 {
|
||||
// text-align: left;
|
||||
// }
|
||||
//
|
||||
// .alert {
|
||||
// padding: 10px;
|
||||
// font-size: 12px;
|
||||
// margin-bottom: 20px;
|
||||
// display: block;
|
||||
// text-align: center;
|
||||
//
|
||||
// i {
|
||||
// margin-right: 5px;
|
||||
// }
|
||||
//
|
||||
// a {
|
||||
// margin: 0;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// .loading {
|
||||
// color: $gscaleF;
|
||||
// animation-duration: 2s;
|
||||
// animation-iteration-count: infinite;
|
||||
// animation-name: blinkfade;
|
||||
// }
|
||||
//
|
||||
// ul.cast-devices {
|
||||
// list-style: none;
|
||||
// margin: 0;
|
||||
// padding: 0;
|
||||
//
|
||||
// li {
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// justify-content: flex-start;
|
||||
// align-items: center;
|
||||
// list-style: none;
|
||||
// border-bottom: 1px solid $gscale2;
|
||||
// border-radius: $baseRadius;
|
||||
//
|
||||
// a {
|
||||
// flex: 1;
|
||||
// display: flex;
|
||||
// flex-direction: row;
|
||||
// justify-content: flex-start;
|
||||
// align-items: center;
|
||||
// padding: 20px 15px;
|
||||
// color: $gscaleF;
|
||||
// align-self: stretch;
|
||||
//
|
||||
// i {
|
||||
//
|
||||
// margin-right: 10px;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// &:hover {
|
||||
// background: $gscale2;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// li:last-child {
|
||||
// border: none;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
.modal-slide {
|
||||
h2 {
|
||||
@ -98,12 +148,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.preview-holder {
|
||||
position: relative;
|
||||
|
||||
.form-group {
|
||||
flex-grow: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hover-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.hover-only {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
||||
&:hover {
|
||||
background: $gkscaleC;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
.view-playlist-list main .main-container {
|
||||
|
||||
.page-content {
|
||||
.inner {
|
||||
h3.divide {
|
||||
margin-top: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slides-holder {
|
||||
|
||||
ul.slides {
|
||||
@ -107,7 +115,7 @@
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin: 0 10px 0 10px;
|
||||
background: $gscale0B;
|
||||
background: $gscale1B;
|
||||
padding: 10px;
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
|
||||
@ -15,9 +15,35 @@
|
||||
}
|
||||
|
||||
.tile-tail {
|
||||
a:last-child {
|
||||
.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tile-metrics {
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.widget,
|
||||
.form-group {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
max-width: 320px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ $lightGrey: rgb(153, 153, 153);
|
||||
$white: rgb(255, 255, 255);
|
||||
$black: rgb(0, 0, 0);
|
||||
$yellow: rgb(229, 161, 35);
|
||||
$orange: rgb(229, 103, 35);
|
||||
$systemSelect: rgb(0, 153, 255);
|
||||
|
||||
// Brand Colors
|
||||
|
||||
@ -24,8 +24,8 @@
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
@mixin pixel-box($pixelOffset: 1) {
|
||||
box-shadow: #{$pixelOffset}px 0 0 $white, 0 #{$pixelOffset}px 0 $limeGreen, -#{$pixelOffset}px 0 0 $seaBlue, 0 -#{$pixelOffset}px 0 $pinkyRed;
|
||||
@mixin pixel-box($pixelOffset: 1, $whiteColor: $white) {
|
||||
box-shadow: #{$pixelOffset}px 0 0 $whiteColor, 0 #{$pixelOffset}px 0 $limeGreen, -#{$pixelOffset}px 0 0 $seaBlue, 0 -#{$pixelOffset}px 0 $pinkyRed;
|
||||
}
|
||||
|
||||
@mixin generate-color-classes($color-map) {
|
||||
@ -83,4 +83,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin generate-alert-classes($color-map) {
|
||||
@each $name, $color in $color-map {
|
||||
.alert.alert-#{"#{$name}"} {
|
||||
color: $color;
|
||||
background: rgba($color, .2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ $info: $seaBlue;
|
||||
$success: $limeGreen;
|
||||
$danger: $pinkyRed;
|
||||
$primary: $seaBlue;
|
||||
$warning: $orange;
|
||||
|
||||
// Common styles
|
||||
$baseRadius: 4px;
|
||||
@ -12,6 +13,8 @@ $layoutBorder: 1px solid $gscale2;
|
||||
|
||||
// Packs
|
||||
$colors: (
|
||||
warning: $warning,
|
||||
orange: $orange,
|
||||
info: $info,
|
||||
info-alt: $bitterBlue,
|
||||
success: $success,
|
||||
@ -37,8 +40,11 @@ $colors: (
|
||||
redhat:$redhat,
|
||||
centos:$centos,
|
||||
other:$other,
|
||||
gscale0:$gscale0,
|
||||
gscaleF:$gscaleF,
|
||||
);
|
||||
|
||||
// Classes
|
||||
@include generate-color-classes($colors);
|
||||
@include generate-button-classes($colors);
|
||||
@include generate-alert-classes($colors);
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
|
||||
// Greyscales
|
||||
$gscale0: #000000;
|
||||
$gscale04: #141414;
|
||||
$gscale05: #151515;
|
||||
$gscale07: #171717;
|
||||
$gscale0B: #1B1B1B;
|
||||
$gscale07: #070707;
|
||||
$gscale14: #141414;
|
||||
$gscale15: #151515;
|
||||
$gscale17: #171717;
|
||||
$gscale1B: #1B1B1B;
|
||||
$gscale1: #111;
|
||||
$gscale2: #222;
|
||||
$gscale3: #333;
|
||||
@ -19,17 +20,19 @@ $gscaleB: #BBB;
|
||||
$gscaleC: #CCC;
|
||||
$gscaleD: #DDD;
|
||||
$gscaleE: #EEE;
|
||||
$gscaleF4: #E4E4E4;
|
||||
$gscaleF5: #E5E5E5;
|
||||
$gscaleF7: #E7E7E7;
|
||||
$gscaleFB: #EBEBEB;
|
||||
$gscaleE4: #E4E4E4;
|
||||
$gscaleE5: #E5E5E5;
|
||||
$gscaleE7: #E7E7E7;
|
||||
$gscaleEB: #EBEBEB;
|
||||
$gscaleF7: #F7F7F7;
|
||||
$gscaleF: #FFFFFF;
|
||||
|
||||
$gkscale0: #000000;
|
||||
$gkscale04: #141414;
|
||||
$gkscale05: #151515;
|
||||
$gkscale07: #171717;
|
||||
$gkscale0B: #1B1B1B;
|
||||
$gkscale07: #070707;
|
||||
$gkscale14: #141414;
|
||||
$gkscale15: #151515;
|
||||
$gkscale17: #171717;
|
||||
$gkscale1B: #1B1B1B;
|
||||
$gkscale1: #111;
|
||||
$gkscale2: #222;
|
||||
$gkscale3: #333;
|
||||
@ -44,8 +47,9 @@ $gkscaleB: #BBB;
|
||||
$gkscaleC: #CCC;
|
||||
$gkscaleD: #DDD;
|
||||
$gkscaleE: #EEE;
|
||||
$gkscaleF4: #E4E4E4;
|
||||
$gkscaleF5: #E5E5E5;
|
||||
$gkscaleF7: #E7E7E7;
|
||||
$gkscaleFB: #EBEBEB;
|
||||
$gkscaleE4: #E4E4E4;
|
||||
$gkscaleE5: #E5E5E5;
|
||||
$gkscaleE7: #E7E7E7;
|
||||
$gkscaleEB: #EBEBEB;
|
||||
$gkscaleF7: #F7F7F7;
|
||||
$gkscaleF: #FFFFFF;
|
||||
@ -1,10 +1,11 @@
|
||||
|
||||
// Greyscales
|
||||
$gscale0: #FFFFFF;
|
||||
$gscale04: #E4E4E4;
|
||||
$gscale05: #E5E5E5;
|
||||
$gscale07: #E7E7E7;
|
||||
$gscale0B: #EBEBEB;
|
||||
$gscale07: #F7F7F7;
|
||||
$gscale1B: #EBEBEB;
|
||||
$gscale17: #E7E7E7;
|
||||
$gscale15: #E5E5E5;
|
||||
$gscale14: #E4E4E4;
|
||||
$gscale1: #EEE;
|
||||
$gscale2: #DDD;
|
||||
$gscale3: #CCC;
|
||||
@ -19,17 +20,19 @@ $gscaleB: #444;
|
||||
$gscaleC: #333;
|
||||
$gscaleD: #222;
|
||||
$gscaleE: #111;
|
||||
$gscaleF4: #141414;
|
||||
$gscaleF5: #151515;
|
||||
$gscaleF7: #171717;
|
||||
$gscaleFB: #1B1B1B;
|
||||
$gscaleE4: #141414;
|
||||
$gscaleE5: #151515;
|
||||
$gscaleE7: #171717;
|
||||
$gscaleEB: #1B1B1B;
|
||||
$gscaleF7: #070707;
|
||||
$gscaleF: #000000;
|
||||
|
||||
$gkscale0: #000000;
|
||||
$gkscale04: #141414;
|
||||
$gkscale05: #151515;
|
||||
$gkscale07: #171717;
|
||||
$gkscale0B: #1B1B1B;
|
||||
$gkscale07: #070707;
|
||||
$gkscale14: #141414;
|
||||
$gkscale15: #151515;
|
||||
$gkscale17: #171717;
|
||||
$gkscale1B: #1B1B1B;
|
||||
$gkscale1: #111;
|
||||
$gkscale2: #222;
|
||||
$gkscale3: #333;
|
||||
@ -44,8 +47,9 @@ $gkscaleB: #BBB;
|
||||
$gkscaleC: #CCC;
|
||||
$gkscaleD: #DDD;
|
||||
$gkscaleE: #EEE;
|
||||
$gkscaleF4: #E4E4E4;
|
||||
$gkscaleF5: #E5E5E5;
|
||||
$gkscaleF7: #E7E7E7;
|
||||
$gkscaleFB: #EBEBEB;
|
||||
$gkscaleE4: #E4E4E4;
|
||||
$gkscaleE5: #E5E5E5;
|
||||
$gkscaleE7: #E7E7E7;
|
||||
$gkscaleEB: #EBEBEB;
|
||||
$gkscaleF7: #F7F7F7;
|
||||
$gkscaleF: #FFFFFF;
|
||||
@ -5,14 +5,13 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: obscreen
|
||||
restart: unless-stopped
|
||||
image: obscreen:latest
|
||||
environment:
|
||||
- DEBUG=${DEBUG-false}
|
||||
- PORT=${PORT-5000}
|
||||
- PLAYER_AUTOSTART_FILE=/app/var/run/play
|
||||
- SECRET_KEY=${SECRET_KEY-ANY_SECRET_KEY_HERE}
|
||||
- DEMO=false
|
||||
- DEBUG=false
|
||||
- SECRET_KEY=ANY_SECRET_KEY_HERE
|
||||
- PORT=5000
|
||||
volumes:
|
||||
- .:/app
|
||||
- ${PLAYER_AUTOSTART_FILE-/dev/null}:/app/var/run/play
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- ./:/app/
|
||||
ports:
|
||||
- ${PORT}:${PORT}
|
||||
- 5000:5000
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
services:
|
||||
webapp:
|
||||
container_name: obscreen
|
||||
restart: unless-stopped
|
||||
image: jierka/obscreen:latest
|
||||
environment:
|
||||
- DEBUG=false
|
||||
- PORT=5000
|
||||
- PLAYER_AUTOSTART_FILE=/app/var/run/play
|
||||
- SECRET_KEY=ANY_SECRET_KEY_HERE
|
||||
volumes:
|
||||
- /dev/null:/app/var/run/play
|
||||
- ./data/db:/app/data/db
|
||||
- ./data/uploads:/app/data/uploads
|
||||
ports:
|
||||
- 5000:5000
|
||||
@ -2,15 +2,16 @@ services:
|
||||
webapp:
|
||||
container_name: obscreen
|
||||
restart: unless-stopped
|
||||
image: jierka/obscreen:latest
|
||||
image: csmith1865/obscreen:latest
|
||||
environment:
|
||||
- DEMO=false
|
||||
- DEBUG=false
|
||||
- PORT=5000
|
||||
- PLAYER_AUTOSTART_FILE=/app/var/run/play
|
||||
- SECRET_KEY=ANY_SECRET_KEY_HERE
|
||||
- PORT=5000
|
||||
volumes:
|
||||
- ./var/run/play:/app/var/run/play
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- ./data/db:/app/data/db
|
||||
- ./data/uploads:/app/data/uploads
|
||||
- ./var/run/storage:/app/var/run/storage
|
||||
ports:
|
||||
- 5000:5000
|
||||
|
||||
42
docker/nginx/nginx.conf
Normal file
42
docker/nginx/nginx.conf
Normal file
@ -0,0 +1,42 @@
|
||||
user nginx;
|
||||
worker_processes 4;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log /var/log/nginx/access.log;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 200G;
|
||||
|
||||
autoindex on;
|
||||
|
||||
server {
|
||||
root /var/www/html/public;
|
||||
listen 80 default_server;
|
||||
listen 443 ssl default_server;
|
||||
|
||||
ssl_certificate /ssl/ssl-cert-snakeoil.pem;
|
||||
ssl_certificate_key /ssl/ssl-cert-snakeoil.key;
|
||||
|
||||
location / {
|
||||
proxy_connect_timeout 60;
|
||||
proxy_read_timeout 60;
|
||||
proxy_send_timeout 60;
|
||||
proxy_intercept_errors on;
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
docs/screenshot-light-mode.png
Normal file
BIN
docs/screenshot-light-mode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 359 KiB |
@ -2,18 +2,24 @@
|
||||
|
||||
> #### 👈 [back to readme](/README.md)
|
||||
|
||||
#### 🔵 You just want a slideshow manager and you'll deal with screen and browser yourself ? You're in the right place.
|
||||
#### 🔵 You just want a slideshow manager, and you'll deal with screen and browser yourself ? You're in the right place.
|
||||
|
||||
|
||||
---
|
||||
## 📡 Run the studio instance
|
||||
|
||||
### with docker run
|
||||
> ⚠️ `docker ... --rm` option is not suitable for production use because it won't survive a reboot. However, it's okay for quick testing. You need to use --restart=always instead to ensure that it persists.
|
||||
<details closed>
|
||||
<summary><h3>Using docker run</h3></summary>
|
||||
|
||||
```bash
|
||||
# (Optional) Install docker if needed
|
||||
curl -sSL get.docker.com | sh && sudo usermod -aG docker $(whoami) && logout # then login again
|
||||
curl -sSL get.docker.com | sh && sudo usermod -aG docker $(whoami) && logout
|
||||
# ....then login again
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
# Prepare application data file tree
|
||||
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
||||
|
||||
@ -21,137 +27,136 @@ cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
||||
docker run --restart=always --name obscreen --pull=always \
|
||||
-e DEBUG=false \
|
||||
-e PORT=5000 \
|
||||
-e PLAYER_AUTOSTART_FILE=/app/var/run/play \
|
||||
-e SECRET_KEY=ANY_SECRET_KEY_HERE \
|
||||
-p 5000:5000 \
|
||||
-v ./data/db:/app/data/db \
|
||||
-v ./data/uploads:/app/data/uploads \
|
||||
-v /dev/null:/app/var/run/play \
|
||||
jierka/obscreen:latest
|
||||
```
|
||||
|
||||
---
|
||||
### or with docker compose
|
||||
|
||||
</details>
|
||||
|
||||
<details closed>
|
||||
<summary><h3>Using docker compose</h3></summary>
|
||||
|
||||
```bash
|
||||
# Prepare application data file tree
|
||||
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
||||
|
||||
# Download docker-compose.yml
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/docker-compose.headless.yml > docker-compose.yml
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/docker-compose.yml > docker-compose.yml
|
||||
|
||||
# Run
|
||||
docker compose up --detach --pull=always
|
||||
```
|
||||
|
||||
---
|
||||
### or system wide
|
||||
|
||||
</details>
|
||||
|
||||
<details closed>
|
||||
<summary><h3>System-wide</h3></summary>
|
||||
|
||||
#### Install
|
||||
- Install studio by executing following script
|
||||
|
||||
##### Linux
|
||||
```bash
|
||||
# Install system dependencies
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git python3-pip python3-venv libsqlite3-dev
|
||||
|
||||
# Get files
|
||||
cd ~ && git clone https://github.com/jr-k/obscreen.git && cd obscreen
|
||||
|
||||
# Install application dependencies
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-studio.sh -o /tmp/install-studio.sh && chmod +x /tmp/install-studio.sh && sudo /bin/bash /tmp/install-studio.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
##### Windows & MacOS
|
||||
```bash
|
||||
git clone https://github.com/jr-k/obscreen.git
|
||||
cd obscreen
|
||||
python3 -m venv venv
|
||||
source ./venv/bin/activate
|
||||
|
||||
# 🚨For MacOS users, requirements installation may cause an error but it's ok if only for pysqlite3 package
|
||||
# you'll need to install brew and execute command `brew install sqlite3`
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Customize server default values
|
||||
pip install .
|
||||
cp .env.dist .env
|
||||
```
|
||||
|
||||
#### Configure
|
||||
- Server configuration is editable in `.env` file.
|
||||
- Application configuration will be available at `http://localhost:5000/settings` page after run.
|
||||
- Check logs with `journalctl -u obscreen-studio -f`
|
||||
|
||||
#### Start server
|
||||
> ⚠️ Not suitable for production use because it won't survive a reboot. However, it's okay for quick testing. You need to use `systemd` (detailed in next section) to ensure that it persists.
|
||||
```bash
|
||||
python ./obscreen.py
|
||||
```
|
||||
|
||||
#### Start server forever with systemd
|
||||
```bash
|
||||
cat "$(pwd)/system/obscreen-studio.service" | sed "s#/home/pi#$HOME#g" | sed "s#=pi#=$USER#g" | sudo tee /etc/systemd/system/obscreen-studio.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable obscreen-studio.service
|
||||
sudo systemctl start obscreen-studio.service
|
||||
```
|
||||
|
||||
#### Troubleshoot
|
||||
```bash
|
||||
# Watch logs with following command
|
||||
sudo journalctl -u obscreen-studio -f
|
||||
```
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 👌 Usage
|
||||
- Page which plays slideshow is reachable at `http://localhost:5000`
|
||||
- Slideshow manager is reachable at `http://localhost:5000/manage`
|
||||
|
||||
|
||||
---
|
||||
## 📺 Run the player instance
|
||||
|
||||
### Autorun for a RaspberryPi
|
||||
- Install player autorun by executing following script (will install chromium, x11 and obscreen-player systemd service)
|
||||
<details closed>
|
||||
<summary><h3>Autorun for a RaspberryPi</h3></summary>
|
||||
|
||||
#### How to install
|
||||
- Install player autorun by executing following script (will install chromium, x11, pulseaudio and obscreen-player systemd service)
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-autorun-rpi.sh | sudo bash -s -- $USER $HOME
|
||||
mkdir -p ~/obscreen/var/run
|
||||
nano ~/obscreen/var/run/play
|
||||
```
|
||||
- Copy following script in `~/obscreen/var/run/play` file to enable chromium autorun (replace `http://localhost:5000` by your own `obscreen-studio` instance url)
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
# Disable screensaver and DPMS
|
||||
xset s off
|
||||
xset -dpms
|
||||
xset s noblank
|
||||
|
||||
# Start unclutter to hide the mouse cursor
|
||||
unclutter -display :0 -noevents -grab &
|
||||
|
||||
# Modify Chromium preferences to avoid restore messages
|
||||
mkdir -p $HOME/.config/chromium/Default 2>/dev/null
|
||||
touch $HOME/.config/chromium/Default/Preferences
|
||||
sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' $HOME/.config/chromium/Default/Preferences
|
||||
|
||||
RESOLUTION=$(DISPLAY=:0 xrandr | grep '*' | awk '{print $1}')
|
||||
WIDTH=$(echo $RESOLUTION | cut -d 'x' -f 1)
|
||||
HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2)
|
||||
|
||||
# Start Chromium in kiosk mode
|
||||
chromium-browser --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=${WIDTH},${HEIGHT} --display=:0 http://localhost:5000
|
||||
```
|
||||
- Restart
|
||||
```bash
|
||||
sudo systemctl restart obscreen-player.service
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-player-rpi.sh -o /tmp/install-player-rpi.sh && chmod +x /tmp/install-player-rpi.sh && sudo /bin/bash /tmp/install-player-rpi.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
### Manually on any device capable of running chromium
|
||||
#### How to restart
|
||||
1. Just use systemctl `sudo systemctl restart obscreen-player.service`
|
||||
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
<details closed>
|
||||
<summary><h3>Manually on any device capable of running chromium</h3></summary>
|
||||
|
||||
When you run the browser yourself, don't forget to use these flags for chromium browser:
|
||||
```bash
|
||||
# chromium or chromium-browser or even chrome
|
||||
# replace http://localhost:5000 with your obscreen-studio instance url
|
||||
chromium --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=1920,1080 --display=:0 http://localhost:5000
|
||||
chromium \
|
||||
--disk-cache-size=2147483648 \
|
||||
--disable-features=Translate \
|
||||
--ignore-certificate-errors \
|
||||
--disable-web-security \
|
||||
--disable-restore-session-state \
|
||||
--autoplay-policy=no-user-gesture-required \
|
||||
--start-maximized \
|
||||
--allow-running-insecure-content \
|
||||
--remember-cert-error-decisions \
|
||||
--noerrdialogs \
|
||||
--kiosk \
|
||||
--incognito \
|
||||
--window-position=0,0 \
|
||||
--window-size=1920,1080 \
|
||||
--display=:0 \
|
||||
http://localhost:5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 📎 Additional
|
||||
|
||||
|
||||
### How to upgrade `obscreen-studio`
|
||||
>#### with docker run
|
||||
- Just add `--pull=always` to your `docker run ...` command, you'll get latest version automatically.
|
||||
>#### or with docker compose
|
||||
- Just add `--pull=always` to your `docker compose up ...` command, , you'll get latest version automatically.
|
||||
>#### or system wide
|
||||
- Execute following script
|
||||
<details closed>
|
||||
<summary><h3>How to upgrade studio instance</h3></summary>
|
||||
|
||||
#### with docker run
|
||||
- Just add `--pull=always` to your `docker run ...` command, you'll get the latest version automatically.
|
||||
#### or with docker compose
|
||||
- Just add `--pull=always` to your `docker compose up ...` command, you'll get the latest version automatically.
|
||||
#### or system-wide
|
||||
- Using Git Updater plugin
|
||||
- Or by executing following script
|
||||
```bash
|
||||
cd ~/obscreen
|
||||
git pull
|
||||
@ -159,3 +164,7 @@ source ./venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
sudo systemctl restart obscreen-studio.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# <img src="https://github.com/jr-k/obscreen/blob/master/docs/img/obscreen.png" width="22"> Obscreen - Autorun on RaspberryPi
|
||||
# <img src="https://raw.githubusercontent.com/csmith1865/obscreen/refs/heads/master/docs/img/obscreen.png" width="22"> Obscreen - Autorun on RaspberryPi
|
||||
|
||||
> #### 👈 [back to readme](/README.md)
|
||||
> #### 👈 [back to readme](../README.md)
|
||||
|
||||
#### 🔴 You want to power RaspberryPi and automatically see your slideshow on a screen connected to it and manage your slideshow ? You're in the right place.
|
||||
|
||||
@ -12,154 +12,152 @@
|
||||
2. Log into your RaspberryPi locally or via ssh (by default it's `ssh pi@raspberrypi.local`)
|
||||
|
||||
|
||||
---
|
||||
## 📡 Run the studio instance
|
||||
|
||||
### with docker run
|
||||
> ⚠️ `docker ... --rm` option is not suitable for production use because it won't survive a reboot. However, it's okay for quick testing. You need to use --restart=always instead to ensure that it persists.
|
||||
```bash
|
||||
# (Optional) Install docker if needed
|
||||
curl -sSL get.docker.com | sh && sudo usermod -aG docker $(whoami) && logout # then login again
|
||||
<details closed>
|
||||
<summary><h3>System-wide (recommended)</h3></summary>
|
||||
|
||||
# Prepare application data file tree
|
||||
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
||||
|
||||
# Prepare player autostart file
|
||||
mkdir -p var/run && touch var/run/play && chmod +x var/run/play
|
||||
|
||||
# Run the Docker container
|
||||
docker run --rm --name obscreen --pull=always \
|
||||
-e DEBUG=false \
|
||||
-e PORT=5000 \
|
||||
-e PLAYER_AUTOSTART_FILE=/app/var/run/play \
|
||||
-e SECRET_KEY=ANY_SECRET_KEY_HERE \
|
||||
-p 5000:5000 \
|
||||
-v ./data/db:/app/data/db \
|
||||
-v ./data/uploads:/app/data/uploads \
|
||||
-v ./var/run/play:/app/var/run/play \
|
||||
jierka/obscreen:latest
|
||||
```
|
||||
---
|
||||
### or with docker compose
|
||||
```bash
|
||||
# Prepare application data file tree
|
||||
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads obscreen/system && cd obscreen
|
||||
|
||||
# Prepare player autostart file
|
||||
mkdir -p var/run && touch var/run/play && chmod +x var/run/play
|
||||
|
||||
# Download docker-compose.yml
|
||||
curl https://raw.githubusercontent.com/jr-k/obscreen/master/docker-compose.yml > docker-compose.yml
|
||||
|
||||
# Run
|
||||
docker compose up --detach --pull=always
|
||||
```
|
||||
---
|
||||
### or system wide
|
||||
#### Install
|
||||
- Install studio by executing following script
|
||||
|
||||
##### Linux
|
||||
```bash
|
||||
# Install system dependencies
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git python3-pip python3-venv libsqlite3-dev
|
||||
|
||||
# Get files
|
||||
cd ~ && git clone https://github.com/jr-k/obscreen.git && cd obscreen
|
||||
|
||||
# Install application dependencies
|
||||
curl -fsSL https://raw.githubusercontent.com/csmith1865/obscreen/master/system/install-studio.sh -o /tmp/install-studio.sh && chmod +x /tmp/install-studio.sh && sudo /bin/bash /tmp/install-studio.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
##### Windows & MacOS
|
||||
```bash
|
||||
git clone https://github.com/csmith1865/obscreen.git
|
||||
cd obscreen
|
||||
python3 -m venv venv
|
||||
source ./venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Customize server default values
|
||||
pip install .
|
||||
cp .env.dist .env
|
||||
```
|
||||
|
||||
#### Configure
|
||||
- Server configuration is editable in `.env` file.
|
||||
- Application configuration will be available at `http://raspberrypi.local:5000/settings` page after run.
|
||||
|
||||
#### Start server
|
||||
> ⚠️ Not suitable for production use because it won't survive a reboot. However, it's okay for quick testing. You need to use `systemd` (detailed in next section) to ensure that it persists.
|
||||
```bash
|
||||
python ./obscreen.py
|
||||
```
|
||||
|
||||
#### Start server forever with systemctl
|
||||
```bash
|
||||
cat "$(pwd)/system/obscreen-studio.service" | sed "s#/home/pi#$HOME#g" | sed "s#=pi#=$USER#g" | sudo tee /etc/systemd/system/obscreen-studio.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable obscreen-studio.service
|
||||
sudo systemctl start obscreen-studio.service
|
||||
```
|
||||
|
||||
#### Troubleshoot
|
||||
```bash
|
||||
# Watch logs with following command
|
||||
sudo journalctl -u obscreen-studio -f
|
||||
```
|
||||
---
|
||||
## 🏁 Finally
|
||||
- Run `sudo systemctl restart obscreen-player` or `sudo reboot`
|
||||
- Check logs with `journalctl -u obscreen-studio -f`
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
<details closed>
|
||||
<summary><h3>Using docker run</h3></summary>
|
||||
|
||||
```bash
|
||||
# (Optional) Install docker if needed
|
||||
curl -sSL get.docker.com | sh && sudo usermod -aG docker $(whoami) && logout
|
||||
# ....then login again
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```bash
|
||||
# Prepare application data file tree
|
||||
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
||||
|
||||
# Run the Docker container
|
||||
docker run --restart=always --name obscreen --pull=always \
|
||||
-e DEBUG=false \
|
||||
-e PORT=5000 \
|
||||
-e SECRET_KEY=ANY_SECRET_KEY_HERE \
|
||||
-p 5000:5000 \
|
||||
-v ./data/db:/app/data/db \
|
||||
-v ./data/uploads:/app/data/uploads \
|
||||
jierka/obscreen:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
<details closed>
|
||||
<summary><h3>Using docker compose</h3></summary>
|
||||
|
||||
```bash
|
||||
# Prepare application data file tree
|
||||
cd ~ && mkdir -p obscreen/data/db obscreen/data/uploads && cd obscreen
|
||||
|
||||
# Download docker-compose.yml
|
||||
curl https://raw.githubusercontent.com/csmith1865/obscreen/master/docker-compose.yml > docker-compose.yml
|
||||
|
||||
# Run
|
||||
docker compose up --detach --pull=always
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 👌 Usage
|
||||
- Page which plays slideshow is reachable at `http://raspberrypi.local:5000`
|
||||
- Slideshow manager is reachable at `http://raspberrypi.local:5000/manage`
|
||||
|
||||
---
|
||||
|
||||
## 📺 Run the player instance
|
||||
|
||||
### Autorun for a RaspberryPi
|
||||
- Install player autorun by executing following script (will install chromium, x11 and obscreen-player systemd service)
|
||||
<details closed>
|
||||
<summary><h3>Autorun for a RaspberryPi</h3></summary>
|
||||
|
||||
#### How to install
|
||||
- Install player autorun by executing following script (will install chromium, x11, pulseaudio and obscreen-player systemd service)
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/jr-k/obscreen/master/system/install-autorun-rpi.sh | sudo bash -s -- $USER $HOME
|
||||
mkdir -p ~/obscreen/var/run
|
||||
nano ~/obscreen/var/run/play
|
||||
```
|
||||
- Copy following script in `~/obscreen/var/run/play` file to enable chromium autorun (replace `http://localhost:5000` by your own `obscreen-studio` instance url)
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
# Disable screensaver and DPMS
|
||||
xset s off
|
||||
xset -dpms
|
||||
xset s noblank
|
||||
|
||||
# Start unclutter to hide the mouse cursor
|
||||
unclutter -display :0 -noevents -grab &
|
||||
|
||||
# Modify Chromium preferences to avoid restore messages
|
||||
mkdir -p /home/pi/.config/chromium/Default 2>/dev/null
|
||||
touch /home/pi/.config/chromium/Default/Preferences
|
||||
sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' /home/pi/.config/chromium/Default/Preferences
|
||||
|
||||
RESOLUTION=$(DISPLAY=:0 xrandr | grep '*' | awk '{print $1}')
|
||||
WIDTH=$(echo $RESOLUTION | cut -d 'x' -f 1)
|
||||
HEIGHT=$(echo $RESOLUTION | cut -d 'x' -f 2)
|
||||
|
||||
# Start Chromium in kiosk mode
|
||||
chromium-browser --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=${WIDTH},${HEIGHT} --display=:0 http://localhost:5000
|
||||
```
|
||||
- Restart
|
||||
```bash
|
||||
sudo systemctl restart obscreen-player.service
|
||||
curl -fsSL https://raw.githubusercontent.com/csmith1865/obscreen/master/system/install-player-rpi.sh -o /tmp/install-player-rpi.sh && chmod +x /tmp/install-player-rpi.sh && sudo /bin/bash /tmp/install-player-rpi.sh $USER $HOME
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
### Manually on any device capable of running chromium
|
||||
#### How to restart
|
||||
1. Just use systemctl `sudo systemctl restart obscreen-player.service`
|
||||
|
||||
#### How to enable sound
|
||||
1. First you have to reboot your device if you never did after obscreen player installation; with command `sudo reboot`
|
||||
2. You have to set audio channel to HDMI `sudo raspi-config nonint do_audio 1` (0 is for jack 3.5 output)
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
<details closed>
|
||||
<summary><h3>Manually on any device capable of running chromium</h3></summary>
|
||||
|
||||
When you run the browser yourself, don't forget to use these flags for chromium browser:
|
||||
```bash
|
||||
# chromium or chromium-browser or even chrome
|
||||
# replace http://localhost:5000 with your obscreen-studio instance url
|
||||
chromium --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=1920,1080 --display=:0 http://localhost:5000
|
||||
chromium \
|
||||
--disk-cache-size=2147483648 \
|
||||
--disable-features=Translate \
|
||||
--ignore-certificate-errors \
|
||||
--disable-web-security \
|
||||
--disable-restore-session-state \
|
||||
--autoplay-policy=no-user-gesture-required \
|
||||
--start-maximized \
|
||||
--allow-running-insecure-content \
|
||||
--remember-cert-error-decisions \
|
||||
--noerrdialogs \
|
||||
--kiosk \
|
||||
--incognito \
|
||||
--window-position=0,0 \
|
||||
--window-size=1920,1080 \
|
||||
--display=:0 \
|
||||
http://localhost:5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ You are done now :)
|
||||
- If everything is set up correctly, the RaspberryPi shall start chromium in fullscreen directly after boot screen and after some seconds of showing the date & time (`views/player/default.jinja.html`) your slideshow shall start and loop endlessly.
|
||||
- Make sure that `PLAYER_AUTOSTART_FILE` exists and is writeable !
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
## 📎 Additional
|
||||
|
||||
<details closed>
|
||||
<summary><h3>Hardware checks</h3></summary>
|
||||
|
||||
### Hardware checks
|
||||
- Basic Setup
|
||||
For basic RaspberryPi setup you can use most of the available guides, for example this one:
|
||||
@ -171,13 +169,20 @@ https://www.raspberrypi.org/documentation/configuration/config-txt/video.md
|
||||
|
||||
However, I used this one: `(2,82) = 1920x1080 60Hz 1080p`
|
||||
|
||||
### How to upgrade `obscreen-studio`
|
||||
>#### with docker run
|
||||
- Just add `--pull=always` to your `docker run ...` command, you'll get latest version automatically.
|
||||
>#### or with docker compose
|
||||
- Just add `--pull=always` to your `docker compose up ...` command, , you'll get latest version automatically.
|
||||
>#### or system wide
|
||||
- Execute following script
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
<details closed>
|
||||
<summary><h3>How to upgrade studio instance</h3></summary>
|
||||
|
||||
#### with docker run
|
||||
- Just add `--pull=always` to your `docker run ...` command, you'll get the latest version automatically.
|
||||
#### or with docker compose
|
||||
- Just add `--pull=always` to your `docker compose up ...` command, you'll get the latest version automatically.
|
||||
#### or system-wide
|
||||
- Using Git Updater plugin
|
||||
- Or by executing following script
|
||||
```bash
|
||||
cd ~/obscreen
|
||||
git pull
|
||||
@ -186,3 +191,6 @@ pip install -r requirements.txt
|
||||
sudo systemctl restart obscreen-studio.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
</details>
|
||||
|
||||
55
lang/en.json
55
lang/en.json
@ -16,6 +16,7 @@
|
||||
"slideshow_slide_panel_th_content": "Content",
|
||||
"slideshow_slide_panel_th_duration": "Ends after",
|
||||
"slideshow_slide_panel_th_duration_unit": "sec",
|
||||
"slideshow_slide_panel_th_delegate_duration_video": "Video's duration",
|
||||
"slideshow_slide_panel_th_enabled": "Enabled",
|
||||
"slideshow_slide_panel_th_cron_scheduled": "Scheduled Start",
|
||||
"slideshow_slide_panel_th_activity": "Options",
|
||||
@ -24,13 +25,16 @@
|
||||
"slideshow_slide_panel_td_cron_scheduled_date": "Date",
|
||||
"slideshow_slide_panel_td_cron_scheduled_bad_cron": "Bad cron value",
|
||||
"slideshow_slide_form_add_title": "Add Slide",
|
||||
"slideshow_slide_form_add_notification_title": "Add Notification",
|
||||
"slideshow_slide_form_edit_notification_title": "Edit Notification",
|
||||
"slideshow_slide_form_add_submit": "Add",
|
||||
"slideshow_slide_form_edit_title": "Edit Slide",
|
||||
"slideshow_slide_form_edit_submit": "Save",
|
||||
"slideshow_slide_form_section_content": "Media",
|
||||
"slideshow_slide_form_section_scheduling": "Scheduling",
|
||||
"slideshow_slide_form_label_name": "Name",
|
||||
"slideshow_slide_form_label_enabled": "Enable/Disable slide",
|
||||
"slideshow_slide_form_label_enabled": "Enable/Disable",
|
||||
"slideshow_slide_form_label_delegate_duration": "Use video's duration",
|
||||
"slideshow_slide_form_label_add_content": "Upload to library",
|
||||
"slideshow_slide_form_label_from_library": "From library",
|
||||
"slideshow_slide_form_label_content_id": "Content",
|
||||
@ -44,6 +48,7 @@
|
||||
"slideshow_slide_form_label_cron_scheduled_end": "End",
|
||||
"slideshow_slide_form_label_cron_scheduled_loop": "Always in loop",
|
||||
"slideshow_slide_form_label_cron_scheduled_duration": "Duration",
|
||||
"slideshow_slide_form_label_cron_scheduled_inweek": "Moment in week",
|
||||
"slideshow_slide_form_label_cron_scheduled_stayloop": "Follow the loop",
|
||||
"slideshow_slide_form_label_cron_scheduled_duration_unit": "seconds",
|
||||
"slideshow_slide_form_label_cron_scheduled_datetime": "Date & Time",
|
||||
@ -54,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "Are you sure?",
|
||||
"slideshow_content_page_title": "Content Library",
|
||||
"slideshow_content_button_add": "New Content",
|
||||
"slideshow_content_referenced_in_slide_error": "Content is referenced in a slide, remove slide first",
|
||||
"slideshow_content_referenced_in_slide_error": "Content '%contentName%' is referenced in a slide, remove slide first",
|
||||
"slideshow_content_panel_active": "Content",
|
||||
"slideshow_content_panel_empty": "Currently, there are no content. %link% now.",
|
||||
"slideshow_content_panel_th_name": "Name",
|
||||
@ -75,8 +80,11 @@
|
||||
"playlist_button_add": "Add Playlist",
|
||||
"playlist_button_delete": "Delete Playlist",
|
||||
"playlist_panel_about_playlist": "About playlist",
|
||||
"playlist_panel_content_management": "Content management",
|
||||
"playlist_panel_content_management": "Playlist loop",
|
||||
"playlist_panel_content_management_desc": "You can add, modify, delete and rearrange the order of contents from this list. Disabled slides will not play in the slideshow.",
|
||||
"playlist_panel_content_management_notifications": "Playlist notifications",
|
||||
"playlist_panel_content_management_notifications_desc": "Notifications are slides that appear on top of everything at a time and for a duration you choose. The order in which they appear below is irrelevant.",
|
||||
"slideshow_slide_button_add_notification": "Add Notification",
|
||||
"playlist_panel_preview": "Playlist preview",
|
||||
"playlist_panel_preview_action": "Preview",
|
||||
"playlist_panel_inactive": "Inactive playlists",
|
||||
@ -97,6 +105,7 @@
|
||||
"js_playlist_delete_confirmation": "Are you sure?",
|
||||
"playlist_delete_has_slides": "Playlist has slides, please remove them before and retry",
|
||||
"playlist_delete_has_node_player_groups": "Playlist is linked to a playgroup",
|
||||
"playlist_cast_warning": "Your <a href=\"%href%\" target=\"_blank\">external URL</a> must be served over https for this to work",
|
||||
"fleet_node_player_page_title": "Players",
|
||||
"fleet_node_player_button_add": "Add a player",
|
||||
"fleet_node_player_panel_active": "Active players",
|
||||
@ -173,13 +182,14 @@
|
||||
"settings_variable_desc_fleet_player_enabled": "Enable fleet player management",
|
||||
"settings_variable_desc_edition_fleet_player_enabled": "Playlist management will also be enabled",
|
||||
"settings_variable_desc_auth_enabled": "Enable auth management",
|
||||
"settings_variable_desc_edition_auth_enabled": "Default user credentials will be admin/admin",
|
||||
"settings_variable_desc_edition_auth_enabled": "Default user credentials will be %username%/%password%",
|
||||
"settings_variable_desc_external_url": "External url (i.e: https://studio-01.company.com or http://10.10.3.100)",
|
||||
"settings_variable_desc_slide_upload_limit": "Slide upload limit (in megabytes)",
|
||||
"settings_variable_desc_dark_mode": "Dark mode",
|
||||
"settings_variable_desc_intro_slide_duration": "Introduction slide duration (in seconds)",
|
||||
"settings_variable_desc_default_slide_time_with_seconds": "Show the seconds on the clock in the introduction slide",
|
||||
"settings_variable_desc_polling_interval": "Refresh interval applied for settings to the player (in seconds)",
|
||||
"settings_variable_desc_player_content_cache": "Enable cache",
|
||||
"settings_variable_desc_slide_animation_enabled": "Enable animation effect between slides",
|
||||
"settings_variable_desc_slide_animation_entrance_effect": "Slide animation entrance effect",
|
||||
"settings_variable_desc_slide_animation_exit_effect": "Slide animation exit effect (generally better off without it)",
|
||||
@ -221,9 +231,13 @@
|
||||
"basic_month_10": "October",
|
||||
"basic_month_11": "November",
|
||||
"basic_month_12": "December",
|
||||
"common_bad_directory_path": "Directory does not exist in the specified path",
|
||||
"common_bad_file_type": "Bad file type uploaded",
|
||||
"common_restart_needed": "Please restart obscreen studio (or restart the device) for the changes to take effect",
|
||||
"common_pick_element": "Pick an element",
|
||||
"common_untitled": "<untitled>",
|
||||
"common_loading": "Loading...",
|
||||
"common_casting": "Casting...",
|
||||
"common_default_node_player_group": "Default Playgroup",
|
||||
"common_default_playlist": "Default Playlist",
|
||||
"common_unknown_ipaddr": "Unknown IP address",
|
||||
@ -241,12 +255,27 @@
|
||||
"common_apply": "Apply",
|
||||
"common_saved": "Changes have been saved",
|
||||
"common_new_folder": "New Folder",
|
||||
"common_folder_not_empty_error": "Folder isn't empty, you must delete its content first",
|
||||
"common_folder_not_empty_error": "Folder '%folderName%' isn't empty, you must delete its content first",
|
||||
"common_copied": "Element copied in clipboard!",
|
||||
"common_host_placeholder": "raspberrypi.local or 192.168.1.85",
|
||||
"common_reachable_at": "Host",
|
||||
"common_http_error_occured": "Error %code% occured",
|
||||
"common_http_error_413": "Files are too large",
|
||||
"common_width": "Width",
|
||||
"common_height": "Height",
|
||||
"common_position": "Position",
|
||||
"common_angle": "Angle",
|
||||
"common_size": "Dimensions",
|
||||
"composition_elements_heading": "Elements",
|
||||
"composition_element_add": "Add element",
|
||||
"composition_elements_delete_all": "Delete all",
|
||||
"composition_presets": "Presets",
|
||||
"composition_presets_grid_2x2": "Grid 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Screen",
|
||||
"composition_element_x_axis": "X axis",
|
||||
"composition_element_y_axis": "Y axis",
|
||||
"composition_element_match_content_aspect_ratio": "Match content aspect ratio",
|
||||
"logout": "Logout",
|
||||
"login_error_not_found": "Bad credentials",
|
||||
"login_error_bad_credentials": "Bad credentials",
|
||||
@ -269,13 +298,19 @@
|
||||
"enum_variable_section_general": "1. General",
|
||||
"enum_variable_section_player_options": "2. Player options",
|
||||
"enum_variable_section_player_animation": "3. Player animation",
|
||||
"enum_variable_section_playlist": "4. Playlists",
|
||||
"enum_variable_section_fleet": "5. Fleet management",
|
||||
"enum_variable_section_security": "6. Security",
|
||||
"enum_variable_section_fleet": "4. Fleet management",
|
||||
"enum_variable_section_security": "5. Security",
|
||||
"enum_application_language_english": "English",
|
||||
"enum_application_language_french": "French",
|
||||
"enum_application_language_italian": "Italian",
|
||||
"enum_application_language_spanish": "Spanish",
|
||||
"enum_content_type_external_storage": "External Storage",
|
||||
"enum_content_type_external_storage_object_label": "Specify an existing directory relative to the following path",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Path relative to a removeable device",
|
||||
"enum_content_type_composition": "Composition",
|
||||
"enum_content_type_composition_object_label": "Screen aspect ratio",
|
||||
"enum_content_type_text": "Text",
|
||||
"enum_content_type_text_object_label": "Displayed text",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Picture",
|
||||
@ -294,8 +329,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Other",
|
||||
"sysinfo_rpi_model": "Raspberry Pi Model",
|
||||
"sysinfo_rpi_model_unknown": "Not a Raspberry Pi or model information not available",
|
||||
"sysinfo_device_model": "Device model",
|
||||
"sysinfo_device_model_unknown": "Unknown model",
|
||||
"sysinfo_storage_free_space": "Storage Free Space",
|
||||
"sysinfo_memory_usage": "Memory Usage",
|
||||
"sysinfo_os_version": "OS Version",
|
||||
|
||||
55
lang/es.json
55
lang/es.json
@ -16,6 +16,7 @@
|
||||
"slideshow_slide_panel_th_content": "Contenido",
|
||||
"slideshow_slide_panel_th_duration": "Termina después",
|
||||
"slideshow_slide_panel_th_duration_unit": "seg",
|
||||
"slideshow_slide_panel_th_delegate_duration_video": "Duración del vídeo",
|
||||
"slideshow_slide_panel_th_enabled": "Habilitado",
|
||||
"slideshow_slide_panel_th_cron_scheduled": "Inicio Programado",
|
||||
"slideshow_slide_panel_th_activity": "Opciones",
|
||||
@ -24,6 +25,8 @@
|
||||
"slideshow_slide_panel_td_cron_scheduled_date": "Fecha",
|
||||
"slideshow_slide_panel_td_cron_scheduled_bad_cron": "Valor de cron incorrecto",
|
||||
"slideshow_slide_form_add_title": "Agregar Diapositiva",
|
||||
"slideshow_slide_form_add_notification_title": "Agregar Notificatión",
|
||||
"slideshow_slide_form_edit_notification_title": "Editar Notificatión",
|
||||
"slideshow_slide_form_add_submit": "Agregar",
|
||||
"slideshow_slide_form_edit_title": "Editar Diapositiva",
|
||||
"slideshow_slide_form_edit_submit": "Guardar",
|
||||
@ -31,7 +34,8 @@
|
||||
"slideshow_slide_form_section_scheduling": "Programación",
|
||||
"slideshow_slide_form_label_name": "Nombre",
|
||||
"slideshow_slide_form_label_add_content": "Upload a la biblioteca",
|
||||
"slideshow_slide_form_label_enabled": "Activar/Desactivar diapositiva",
|
||||
"slideshow_slide_form_label_enabled": "Activar/Desactivar",
|
||||
"slideshow_slide_form_label_delegate_duration": "Utilizar la duración del vídeo",
|
||||
"slideshow_slide_form_label_from_library": "Dalla biblioteca",
|
||||
"slideshow_slide_form_label_content_id": "Contenido",
|
||||
"slideshow_slide_form_label_location": "Ubicación",
|
||||
@ -44,6 +48,7 @@
|
||||
"slideshow_slide_form_label_cron_scheduled_end": "Fin",
|
||||
"slideshow_slide_form_label_cron_scheduled_loop": "Siempre en bucle",
|
||||
"slideshow_slide_form_label_cron_scheduled_duration": "Duración",
|
||||
"slideshow_slide_form_label_cron_scheduled_inweek": "Momento de la semana",
|
||||
"slideshow_slide_form_label_cron_scheduled_stayloop": "Seguir el bucle",
|
||||
"slideshow_slide_form_label_cron_scheduled_duration_unit": "segundos",
|
||||
"slideshow_slide_form_label_cron_scheduled_datetime": "Fecha y Hora",
|
||||
@ -54,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "¿Estás seguro?",
|
||||
"slideshow_content_page_title": "Biblioteca de contenidos",
|
||||
"slideshow_content_button_add": "Nuevo Contenido",
|
||||
"slideshow_content_referenced_in_slide_error": "Se hace referencia al contenido en una diapositiva; elimine la diapositiva primero",
|
||||
"slideshow_content_referenced_in_slide_error": "Se hace referencia al contenido '%contentName%' en una diapositiva; elimine la diapositiva primero",
|
||||
"slideshow_content_panel_active": "Contenido",
|
||||
"slideshow_content_panel_empty": "Actualmente, no hay contenido. %link% ahora.",
|
||||
"slideshow_content_panel_th_name": "Nombre",
|
||||
@ -75,8 +80,11 @@
|
||||
"playlist_button_add": "Agregar Playlist",
|
||||
"playlist_button_delete": "Eliminar Playlist",
|
||||
"playlist_panel_about_playlist": "Acerca de la playlist",
|
||||
"playlist_panel_content_management": "Gestión de contenido",
|
||||
"playlist_panel_content_management": "Bucle de playlist ",
|
||||
"playlist_panel_content_management_desc": "Puedes agregar, modificar, eliminar y reorganizar el orden de los contenidos de esta lista. Las diapositivas deshabilitadas no se reproducirán en la presentación de diapositivas.",
|
||||
"playlist_panel_content_management_notifications": "Notificaciones de listas de reproducción",
|
||||
"playlist_panel_content_management_notifications_desc": "Las notificaciones son diapositivas que aparecen encima de todo en un momento y durante el tiempo que elijas. El orden en que aparecen a continuación es irrelevante.",
|
||||
"slideshow_slide_button_add_notification": "Agregar notificación",
|
||||
"playlist_panel_preview": "Vista previa de la playlist",
|
||||
"playlist_panel_preview_action": "Avance",
|
||||
"playlist_panel_inactive": "Playlist inactivas",
|
||||
@ -97,6 +105,7 @@
|
||||
"js_playlist_delete_confirmation": "¿Estás seguro?",
|
||||
"playlist_delete_has_slides": "La playlist tiene diapositivas, por favor elimínelas antes y reintente",
|
||||
"playlist_delete_has_node_player_groups": "La playlist está asignada a un playgroup",
|
||||
"playlist_cast_warning": "Tu <a href=\"%href%\" target=\"_blank\">URL externa</a> debe ser entregada en https para que esto funcione",
|
||||
"fleet_node_player_page_title": "Reproductores",
|
||||
"fleet_node_player_button_add": "Agregar un reproductor",
|
||||
"fleet_node_player_panel_active": "Reproductores activos",
|
||||
@ -174,13 +183,14 @@
|
||||
"settings_variable_desc_fleet_player_enabled": "Habilitar gestión de reproductores de flota",
|
||||
"settings_variable_desc_edition_fleet_player_enabled": "La gestión de playlist también se habilitará",
|
||||
"settings_variable_desc_auth_enabled": "Habilitar gestión de autenticación",
|
||||
"settings_variable_desc_edition_auth_enabled": "Las credenciales predeterminadas del usuario serán admin/admin",
|
||||
"settings_variable_desc_edition_auth_enabled": "Las credenciales predeterminadas del usuario serán %username%/%password%",
|
||||
"settings_variable_desc_external_url": "URL externa (ej.: https://studio-01.company.com o http://10.10.3.100)",
|
||||
"settings_variable_desc_slide_upload_limit": "Límite de carga de diapositivas (en megabytes)",
|
||||
"settings_variable_desc_dark_mode": "Modo oscuro",
|
||||
"settings_variable_desc_intro_slide_duration": "Duración de la diapositiva de introducción (en segundos)",
|
||||
"settings_variable_desc_default_slide_time_with_seconds": "Mostrar los segundos en el reloj de la diapositiva de introducción",
|
||||
"settings_variable_desc_polling_interval": "Intervalo de actualización aplicado para configuraciones del reproductor (en segundos)",
|
||||
"settings_variable_desc_player_content_cache": "Habilitar la caché",
|
||||
"settings_variable_desc_slide_animation_enabled": "Habilitar efecto de animación entre diapositivas",
|
||||
"settings_variable_desc_slide_animation_entrance_effect": "Efecto de entrada de animación de diapositiva",
|
||||
"settings_variable_desc_slide_animation_exit_effect": "Efecto de salida de animación de diapositiva (generalmente mejor sin él)",
|
||||
@ -222,9 +232,13 @@
|
||||
"basic_month_10": "Octubre",
|
||||
"basic_month_11": "Noviembre",
|
||||
"basic_month_12": "Diciembre",
|
||||
"common_bad_directory_path": "El directorio no existe en la ruta especificada",
|
||||
"common_bad_file_type": "Tipo de archivo incorrecto cargado",
|
||||
"common_restart_needed": "Reinicie obscreen studio (o reinicie el dispositivo) para que los cambios surtan efecto",
|
||||
"common_pick_element": "Elige un elemento",
|
||||
"common_untitled": "<sin-título>",
|
||||
"common_loading": "Cargando...",
|
||||
"common_casting": "Casting...",
|
||||
"common_default_node_player_group": "Playgroup predeterminado",
|
||||
"common_default_playlist": "Lista de reproducción predeterminada",
|
||||
"common_unknown_ipaddr": "Dirección IP desconocida",
|
||||
@ -242,12 +256,27 @@
|
||||
"common_apply": "Aplicar",
|
||||
"common_saved": "Los cambios se han guardado",
|
||||
"common_new_folder": "Nuevo Carpeta",
|
||||
"common_folder_not_empty_error": "La carpeta no está vacía, primero debes eliminar su contenido",
|
||||
"common_folder_not_empty_error": "La carpeta '%folderName%' no está vacía, primero debes eliminar su contenido",
|
||||
"common_copied": "¡Elemento copiado!",
|
||||
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
||||
"common_reachable_at": "Host",
|
||||
"common_http_error_occured": "Se ha producido un error %code%",
|
||||
"common_http_error_413": "Los archivos son demasiado grandes",
|
||||
"common_width": "Ancho",
|
||||
"common_height": "Altura",
|
||||
"common_position": "Posición",
|
||||
"common_angle": "Ángulo",
|
||||
"common_size": "Dimensiones",
|
||||
"composition_elements_heading": "Elementos",
|
||||
"composition_element_add": "Añadir elemento",
|
||||
"composition_elements_delete_all": "Eliminar todo",
|
||||
"composition_presets": "Preajustes",
|
||||
"composition_presets_grid_2x2": "Cuadrícula 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Pantalla",
|
||||
"composition_element_x_axis": "Eje X",
|
||||
"composition_element_y_axis": "Eje Y",
|
||||
"composition_element_match_content_aspect_ratio": "Ajustar la escala del contenido",
|
||||
"logout": "Cerrar sesión",
|
||||
"login_error_not_found": "Credenciales incorrectas",
|
||||
"login_error_bad_credentials": "Credenciales incorrectas",
|
||||
@ -270,13 +299,19 @@
|
||||
"enum_variable_section_general": "1. General",
|
||||
"enum_variable_section_player_options": "2. Opciones del reproductor",
|
||||
"enum_variable_section_player_animation": "3. Animación del reproductor",
|
||||
"enum_variable_section_playlist": "4. Playlist",
|
||||
"enum_variable_section_fleet": "5. Gestión de flota",
|
||||
"enum_variable_section_security": "6. Seguridad",
|
||||
"enum_variable_section_fleet": "4. Gestión de flota",
|
||||
"enum_variable_section_security": "5. Seguridad",
|
||||
"enum_application_language_english": "Inglés",
|
||||
"enum_application_language_french": "Francés",
|
||||
"enum_application_language_italian": "Italiano",
|
||||
"enum_application_language_spanish": "Español",
|
||||
"enum_content_type_external_storage": "Almacenamiento externo",
|
||||
"enum_content_type_external_storage_object_label": "Especifique un directorio existente relativo a la siguiente ruta",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Ruta relativa a un dispositivo extraíble",
|
||||
"enum_content_type_composition": "Composición",
|
||||
"enum_content_type_composition_object_label": "Relación de aspecto de la pantalla",
|
||||
"enum_content_type_text": "Texto",
|
||||
"enum_content_type_text_object_label": "Texto mostrado",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Imagen",
|
||||
@ -295,8 +330,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Otro",
|
||||
"sysinfo_rpi_model": "Modelo de Raspberry Pi",
|
||||
"sysinfo_rpi_model_unknown": "No es una Raspberry Pi o la información del modelo no está disponible",
|
||||
"sysinfo_device_model": "Modelo del dispositivo",
|
||||
"sysinfo_device_model_unknown": "Modelo desconocido",
|
||||
"sysinfo_storage_free_space": "Espacio de almacenamiento libre",
|
||||
"sysinfo_memory_usage": "Uso de memoria",
|
||||
"sysinfo_os_version": "Versión del SO",
|
||||
|
||||
57
lang/fr.json
57
lang/fr.json
@ -16,6 +16,7 @@
|
||||
"slideshow_slide_panel_th_content": "Contenu",
|
||||
"slideshow_slide_panel_th_duration": "Fin après",
|
||||
"slideshow_slide_panel_th_duration_unit": "sec",
|
||||
"slideshow_slide_panel_th_delegate_duration_video": "Durée de la vidéo",
|
||||
"slideshow_slide_panel_th_enabled": "Activé",
|
||||
"slideshow_slide_panel_th_cron_scheduled": "Programmation",
|
||||
"slideshow_slide_panel_th_activity": "Options",
|
||||
@ -24,13 +25,16 @@
|
||||
"slideshow_slide_panel_td_cron_scheduled_date": "Date",
|
||||
"slideshow_slide_panel_td_cron_scheduled_bad_cron": "Mauvaise valeur cron",
|
||||
"slideshow_slide_form_add_title": "Ajout d'une slide",
|
||||
"slideshow_slide_form_add_notification_title": "Ajout d'une notification",
|
||||
"slideshow_slide_form_edit_notification_title": "Modification de la notification",
|
||||
"slideshow_slide_form_add_submit": "Ajouter",
|
||||
"slideshow_slide_form_edit_title": "Modification d'une slide",
|
||||
"slideshow_slide_form_edit_title": "Modification de la slide",
|
||||
"slideshow_slide_form_edit_submit": "Enregistrer",
|
||||
"slideshow_slide_form_section_content": "Media",
|
||||
"slideshow_slide_form_section_scheduling": "Programmation",
|
||||
"slideshow_slide_form_label_name": "Nom",
|
||||
"slideshow_slide_form_label_enabled": "Activer/Désactiver la slide",
|
||||
"slideshow_slide_form_label_enabled": "Activer/Désactiver",
|
||||
"slideshow_slide_form_label_delegate_duration": "Utiliser la durée de la vidéo",
|
||||
"slideshow_slide_form_label_add_content": "Upload à la bibliothèque",
|
||||
"slideshow_slide_form_label_from_library": "Depuis la bibliothèque",
|
||||
"slideshow_slide_form_label_content_id": "Contenu",
|
||||
@ -44,6 +48,7 @@
|
||||
"slideshow_slide_form_label_cron_scheduled_end": "Fin",
|
||||
"slideshow_slide_form_label_cron_scheduled_loop": "Toujours en boucle",
|
||||
"slideshow_slide_form_label_cron_scheduled_duration": "Durée",
|
||||
"slideshow_slide_form_label_cron_scheduled_inweek": "Moment de la semaine",
|
||||
"slideshow_slide_form_label_cron_scheduled_stayloop": "Suit la boucle",
|
||||
"slideshow_slide_form_label_cron_scheduled_duration_unit": "secondes",
|
||||
"slideshow_slide_form_label_cron_scheduled_datetime": "Date & Heure",
|
||||
@ -54,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
|
||||
"slideshow_content_page_title": "Bibliothèque de contenus",
|
||||
"slideshow_content_button_add": "Nouveau Contenu",
|
||||
"slideshow_content_referenced_in_slide_error": "Le contenu est référencé dans une slide, supprimez d'abord la slide",
|
||||
"slideshow_content_referenced_in_slide_error": "Le contenu '%contentName%' est référencé dans une slide, supprimez d'abord la slide",
|
||||
"slideshow_content_panel_active": "Contenus",
|
||||
"slideshow_content_panel_empty": "Actuellement, il n'y a aucun contenu. %link% maintenant.",
|
||||
"slideshow_content_panel_th_name": "Nom",
|
||||
@ -75,8 +80,11 @@
|
||||
"playlist_button_add": "Ajouter Playlist",
|
||||
"playlist_button_delete": "Supprimer Playlist",
|
||||
"playlist_panel_about_playlist": "À propos de la playlist",
|
||||
"playlist_panel_content_management": "Elements de la playlist",
|
||||
"playlist_panel_content_management": "Boucle de la playlist",
|
||||
"playlist_panel_content_management_desc": "Vous pouvez ajouter, modifier, supprimer et réorganiser l'ordre des contenus depuis cette liste. Les slides désactivées ne seront pas lues dans le diaporama.",
|
||||
"playlist_panel_content_management_notifications": "Notifications de la playlist",
|
||||
"playlist_panel_content_management_notifications_desc": "Les notifications sont des slides qui apparaissent au dessus de tout à un moment et pendant une durée que vous choisissez. L'ordre dans lequel elles apparaissent ci-dessous n'a aucune importance.",
|
||||
"slideshow_slide_button_add_notification": "Ajouter une notification",
|
||||
"playlist_panel_preview": "Playlist preview",
|
||||
"playlist_panel_preview_action": "Prévisualiser",
|
||||
"playlist_panel_inactive": "Playlist inactives",
|
||||
@ -98,6 +106,7 @@
|
||||
"js_playlist_delete_confirmation": "Êtes-vous sûr ?",
|
||||
"playlist_delete_has_slides": "La playlist contient des slides, supprimez-les avant et réessayez",
|
||||
"playlist_delete_has_node_player_groups": "La playlist est attribuée à un playgroup",
|
||||
"playlist_cast_warning": "Votre <a href=\"%href%\" target=\"_blank\">URL externe</a> doit être servi en https pour que ça fonctionne",
|
||||
"fleet_node_player_page_title": "Lecteurs",
|
||||
"fleet_node_player_button_add": "Ajouter un lecteur",
|
||||
"fleet_node_player_panel_active": "Players actifs",
|
||||
@ -175,13 +184,14 @@
|
||||
"settings_variable_desc_fleet_player_enabled": "Activer la gestion de flotte des players",
|
||||
"settings_variable_desc_edition_fleet_player_enabled": "Les playlists seront également activées",
|
||||
"settings_variable_desc_auth_enabled": "Activer la gestion de l'authentification",
|
||||
"settings_variable_desc_edition_auth_enabled": "Les identifiants de l'utilisateur par défaut seront admin/admin",
|
||||
"settings_variable_desc_edition_auth_enabled": "Les identifiants de l'utilisateur par défaut seront %username%/%password%",
|
||||
"settings_variable_desc_external_url": "URL externe (i.e: https://studio-01.company.com or http://10.10.3.100)",
|
||||
"settings_variable_desc_slide_upload_limit": "Limite d'upload du fichier d'une slide (en mégaoctets)",
|
||||
"settings_variable_desc_dark_mode": "Mdoe sombre",
|
||||
"settings_variable_desc_intro_slide_duration": "Durée de la slide d'introduction (en secondes)",
|
||||
"settings_variable_desc_default_slide_time_with_seconds": "Afficher les secondes de l'horloge de la slide d'introduction",
|
||||
"settings_variable_desc_polling_interval": "Intervalle de rafraîchissement des paramètres à appliquer au lecteur (en secondes)",
|
||||
"settings_variable_desc_player_content_cache": "Activer le cache",
|
||||
"settings_variable_desc_slide_animation_enabled": "Activer les effets d'animation entre les slides",
|
||||
"settings_variable_desc_slide_animation_entrance_effect": "Effet d'animation d'arrivée de la slide",
|
||||
"settings_variable_desc_slide_animation_exit_effect": "Effet d'animation de sortie de la slide (généralement mieux sans)",
|
||||
@ -223,9 +233,13 @@
|
||||
"basic_month_10": "Octobre",
|
||||
"basic_month_11": "Novembre",
|
||||
"basic_month_12": "Décembre",
|
||||
"common_bad_directory_path": "Le dossier n'existe pas dans le chemin indiqué",
|
||||
"common_bad_file_type": "Type de fichier uploadé incorrect",
|
||||
"common_restart_needed": "Veuillez redémarrer obscreen studio (ou redémarrer l'appareil) pour que les changements soient pris en compte",
|
||||
"common_pick_element": "Choisissez un élément",
|
||||
"common_untitled": "<sans-titre>",
|
||||
"common_loading": "Chargement...",
|
||||
"common_casting": "Casting...",
|
||||
"common_default_node_player_group": "Playgroup par défaut",
|
||||
"common_default_playlist": "Playlist par défaut",
|
||||
"common_unknown_ipaddr": "Adresse IP inconnue",
|
||||
@ -243,12 +257,27 @@
|
||||
"common_apply": "Appliquer",
|
||||
"common_saved": "Les modifications ont été enregistrées",
|
||||
"common_new_folder": "Nouveau Dossier",
|
||||
"common_folder_not_empty_error": "Le dossier n'est pas vide, vous devez d'abord supprimer son contenu",
|
||||
"common_folder_not_empty_error": "Le dossier '%folderName%' n'est pas vide, vous devez d'abord supprimer son contenu",
|
||||
"common_copied": "Element copié !",
|
||||
"common_host_placeholder": "raspberrypi.local ou 192.168.1.85",
|
||||
"common_reachable_at": "Hôte",
|
||||
"common_http_error_occured": "Une erreur %code% est apparue",
|
||||
"common_http_error_413": "Les fichiers sont trop volumineux",
|
||||
"common_width": "Largeur",
|
||||
"common_height": "Hauteur",
|
||||
"common_position": "Position",
|
||||
"common_angle": "Angle",
|
||||
"common_size": "Dimensions",
|
||||
"composition_elements_heading": "Éléments",
|
||||
"composition_element_add": "Ajouter un élément",
|
||||
"composition_elements_delete_all": "Tout supprimer",
|
||||
"composition_presets": "Préréglages",
|
||||
"composition_presets_grid_2x2": "Grille 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Écran",
|
||||
"composition_element_x_axis": "Axe X",
|
||||
"composition_element_y_axis": "Axe Y",
|
||||
"composition_element_match_content_aspect_ratio": "Ajuster l'échelle du contenu",
|
||||
"logout": "Déconnexion",
|
||||
"login_error_not_found": "Identifiants invalides",
|
||||
"login_error_bad_credentials": "Identifiants invalides",
|
||||
@ -271,13 +300,19 @@
|
||||
"enum_variable_section_general": "1. Général",
|
||||
"enum_variable_section_player_options": "2. Options du lecteur",
|
||||
"enum_variable_section_player_animation": "3. Animation du lecteur",
|
||||
"enum_variable_section_playlist": "4. Playlists",
|
||||
"enum_variable_section_fleet": "5. Gestion de flotte",
|
||||
"enum_variable_section_security": "6. Sécurité",
|
||||
"enum_variable_section_fleet": "4. Gestion de flotte",
|
||||
"enum_variable_section_security": "5. Sécurité",
|
||||
"enum_application_language_english": "Anglais",
|
||||
"enum_application_language_french": "Français",
|
||||
"enum_application_language_italian": "Italien",
|
||||
"enum_application_language_spanish": "Espagnol",
|
||||
"enum_content_type_external_storage": "Stockage externe",
|
||||
"enum_content_type_external_storage_object_label": "Spécifiez un répertoire existant par rapport au chemin suivant",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Chemin relatif à un périphérique amovible",
|
||||
"enum_content_type_composition": "Composition",
|
||||
"enum_content_type_composition_object_label": "Rapport hauteur/largeur de l'écran",
|
||||
"enum_content_type_text": "Texte",
|
||||
"enum_content_type_text_object_label": "Texte affiché",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Vidéo",
|
||||
"enum_content_type_picture": "Image",
|
||||
@ -296,8 +331,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Autre",
|
||||
"sysinfo_rpi_model": "Modèle du Raspberry Pi",
|
||||
"sysinfo_rpi_model_unknown": "Le modèle n'est pas un Raspberry Pi",
|
||||
"sysinfo_device_model": "Modèle de l'appareil",
|
||||
"sysinfo_device_model_unknown": "Modèle inconnu",
|
||||
"sysinfo_storage_free_space": "Stockage Disponible",
|
||||
"sysinfo_memory_usage": "Utilisation Mémoire",
|
||||
"sysinfo_os_version": "Version SE",
|
||||
|
||||
55
lang/it.json
55
lang/it.json
@ -16,6 +16,7 @@
|
||||
"slideshow_slide_panel_th_content": "Contenuti",
|
||||
"slideshow_slide_panel_th_duration": "Finito in",
|
||||
"slideshow_slide_panel_th_duration_unit": "sec",
|
||||
"slideshow_slide_panel_th_delegate_duration_video": "Durata del video",
|
||||
"slideshow_slide_panel_th_enabled": "Abilitato",
|
||||
"slideshow_slide_panel_th_cron_scheduled": "Avvia programmazione",
|
||||
"slideshow_slide_panel_th_activity": "Opzioni",
|
||||
@ -24,13 +25,16 @@
|
||||
"slideshow_slide_panel_td_cron_scheduled_date": "Data",
|
||||
"slideshow_slide_panel_td_cron_scheduled_bad_cron": "Valore cron errato",
|
||||
"slideshow_slide_form_add_title": "Aggiungi Slide",
|
||||
"slideshow_slide_form_add_notification_title": "Aggiungi Notifica",
|
||||
"slideshow_slide_form_edit_notification_title": "Modifica Notifica",
|
||||
"slideshow_slide_form_add_submit": "Aggiungi",
|
||||
"slideshow_slide_form_edit_title": "Modifica Slide",
|
||||
"slideshow_slide_form_edit_submit": "Salva",
|
||||
"slideshow_slide_form_section_content": "Media",
|
||||
"slideshow_slide_form_section_scheduling": "Programmazione",
|
||||
"slideshow_slide_form_label_name": "Nome",
|
||||
"slideshow_slide_form_label_enabled": "Abilita/Disabilita diapositiva",
|
||||
"slideshow_slide_form_label_enabled": "Abilita/Disabilita",
|
||||
"slideshow_slide_form_label_delegate_duration": "Utilizza la durata del video",
|
||||
"slideshow_slide_form_label_add_content": "Upload alla biblioteca",
|
||||
"slideshow_slide_form_label_from_library": "Dalla biblioteca",
|
||||
"slideshow_slide_form_label_content_id": "Contenuti",
|
||||
@ -44,6 +48,7 @@
|
||||
"slideshow_slide_form_label_cron_scheduled_end": "Fine",
|
||||
"slideshow_slide_form_label_cron_scheduled_loop": "Sempre in loop",
|
||||
"slideshow_slide_form_label_cron_scheduled_duration": "Durata",
|
||||
"slideshow_slide_form_label_cron_scheduled_inweek": "Momento della settimana",
|
||||
"slideshow_slide_form_label_cron_scheduled_stayloop": "Seguire il ciclo",
|
||||
"slideshow_slide_form_label_cron_scheduled_duration_unit": "secondi",
|
||||
"slideshow_slide_form_label_cron_scheduled_datetime": "Data e ora",
|
||||
@ -54,7 +59,7 @@
|
||||
"js_slideshow_slide_delete_confirmation": "Sei sicuro?",
|
||||
"slideshow_content_page_title": "Libreria dei contenuti",
|
||||
"slideshow_content_button_add": "Nuovo Contenuto",
|
||||
"slideshow_content_referenced_in_slide_error": "Si fa riferimento al contenuto in una diapositiva, rimuovere prima la diapositiva",
|
||||
"slideshow_content_referenced_in_slide_error": "Si fa riferimento al contenuto '%contentName%' in una diapositiva, rimuovere prima la diapositiva",
|
||||
"slideshow_content_panel_active": "Contenuti",
|
||||
"slideshow_content_panel_empty": "Attualmente non ci sono contenuti. %link% adesso.",
|
||||
"slideshow_content_panel_th_name": "Nome",
|
||||
@ -75,8 +80,11 @@
|
||||
"playlist_button_add": "Aggiungi Playlist",
|
||||
"playlist_button_delete": "Elimina Playlist",
|
||||
"playlist_panel_about_playlist": "Informazioni sulla playlist",
|
||||
"playlist_panel_content_management": "Gestione dei contenuti",
|
||||
"playlist_panel_content_management": "Ciclo della playlist",
|
||||
"playlist_panel_content_management_desc": "Puoi aggiungere, modificare, eliminare e riorganizzare l'ordine dei contenuti da questo elenco. Le diapositive disabilitate non verranno riprodotte nella presentazione.",
|
||||
"playlist_panel_content_management_notifications": "Notifiche playlist",
|
||||
"playlist_panel_content_management_notifications_desc": "Le notifiche sono diapositive che appaiono sopra ogni cosa alla volta e per la durata da te scelta. L'ordine in cui appaiono di seguito è irrilevante.",
|
||||
"slideshow_slide_button_add_notification": "Aggiungi notifica",
|
||||
"playlist_panel_preview": "Anteprima della playlist",
|
||||
"playlist_panel_preview_action": "Anteprima",
|
||||
"playlist_panel_inactive": "Playlist inattive",
|
||||
@ -97,6 +105,7 @@
|
||||
"js_playlist_delete_confirmation": "Sei sicuro?",
|
||||
"playlist_delete_has_slides": "Sono presenti slide nella playlist, annullale e riprova",
|
||||
"playlist_delete_has_node_player_groups": "La playlist è collegata ad un playgroup",
|
||||
"playlist_cast_warning": "Il tuo <a href=\"%href%\" target=\"_blank\">URL esterno</a> deve essere servito in https affinché funzioni",
|
||||
"fleet_node_player_page_title": "Schermi",
|
||||
"fleet_node_player_button_add": "Aggiungi allo schermo",
|
||||
"fleet_node_player_panel_active": "Schermi attivi",
|
||||
@ -174,13 +183,14 @@
|
||||
"settings_variable_desc_fleet_player_enabled": "Abilita panoramica gestione monitor",
|
||||
"settings_variable_desc_edition_fleet_player_enabled": "Verrà abilitata anche la gestione delle playlist",
|
||||
"settings_variable_desc_auth_enabled": "Abilita la gestione autenticazione",
|
||||
"settings_variable_desc_edition_auth_enabled": "Le credenziali utente predefinite sono admin/admin",
|
||||
"settings_variable_desc_edition_auth_enabled": "Le credenziali utente predefinite sono %username%/%password%",
|
||||
"settings_variable_desc_external_url": "Url esterno (esempio: https://studio-01.company.com or http://10.10.3.100)",
|
||||
"settings_variable_desc_slide_upload_limit": "Limite upload slide (in megabytes)",
|
||||
"settings_variable_desc_dark_mode": "Modalità scura",
|
||||
"settings_variable_desc_intro_slide_duration": "Durata introduzione slide (in secondi)",
|
||||
"settings_variable_desc_default_slide_time_with_seconds": "Mostra secondi introduzione slide",
|
||||
"settings_variable_desc_polling_interval": "Intervallo di aggiornamento applicato per le impostazioni del monitor (in secondi)",
|
||||
"settings_variable_desc_player_content_cache": "Abilita la cache",
|
||||
"settings_variable_desc_slide_animation_enabled": "Abilita l'effetto di animazione tra le diapositive",
|
||||
"settings_variable_desc_slide_animation_entrance_effect": "Effetto ingresso diapositiva",
|
||||
"settings_variable_desc_slide_animation_exit_effect": "Effetto di uscita della diapositiva (meglio senza)",
|
||||
@ -222,9 +232,13 @@
|
||||
"basic_month_10": "Ottobre",
|
||||
"basic_month_11": "Novembre",
|
||||
"basic_month_12": "Dicembre",
|
||||
"common_bad_directory_path": "La directory non esiste nel percorso specificato",
|
||||
"common_bad_file_type": "Tipo di file caricato non valido",
|
||||
"common_restart_needed": "Riavvia obscreen studio (o riavvia il dispositivo) affinché le modifiche abbiano effetto",
|
||||
"common_pick_element": "Scegli un elemento",
|
||||
"common_untitled": "<senza-titolo>",
|
||||
"common_loading": "Caricamento...",
|
||||
"common_casting": "Casting...",
|
||||
"common_default_node_player_group": "Playgroup di default",
|
||||
"common_default_playlist": "Default playlist",
|
||||
"common_unknown_ipaddr": "IP sconosciuto",
|
||||
@ -242,12 +256,27 @@
|
||||
"common_apply": "Applica",
|
||||
"common_saved": "Le modifiche sono state salvate",
|
||||
"common_new_folder": "Nuovo Cartella",
|
||||
"common_folder_not_empty_error": "La cartella non è vuota, devi prima eliminarne il contenuto",
|
||||
"common_folder_not_empty_error": "La cartella '%folderName%' non è vuota, devi prima eliminarne il contenuto",
|
||||
"common_copied": "Elemento copiato!",
|
||||
"common_host_placeholder": "raspberrypi.local o 192.168.1.85",
|
||||
"common_reachable_at": "Host",
|
||||
"common_http_error_occured": "Si è verificato un errore %code%",
|
||||
"common_http_error_413": "I file sono troppo grandi",
|
||||
"common_width": "Larghezza",
|
||||
"common_height": "Altezza",
|
||||
"common_position": "Posizione",
|
||||
"common_angle": "Angolo",
|
||||
"common_size": "Dimensioni",
|
||||
"composition_elements_heading": "Elementi",
|
||||
"composition_element_add": "Aggiungi elemento",
|
||||
"composition_elements_delete_all": "Elimina tutto",
|
||||
"composition_presets": "Preimpostazioni",
|
||||
"composition_presets_grid_2x2": "Griglia 2x2",
|
||||
"composition_presets_tvnews_1x1": "TV news 1x1",
|
||||
"composition_monitor": "Schermo",
|
||||
"composition_element_x_axis": "Asse X",
|
||||
"composition_element_y_axis": "Asse Y",
|
||||
"composition_element_match_content_aspect_ratio": "Regola la scala del contenuto",
|
||||
"logout": "Logout",
|
||||
"login_error_not_found": "Credenziali errate",
|
||||
"login_error_bad_credentials": "Credenziali errate",
|
||||
@ -270,13 +299,19 @@
|
||||
"enum_variable_section_general": "1. Generale",
|
||||
"enum_variable_section_player_options": "2. Opzioni monitor",
|
||||
"enum_variable_section_player_animation": "3. Animazioni monitor",
|
||||
"enum_variable_section_playlist": "4. Playlist",
|
||||
"enum_variable_section_fleet": "5. Gestione panoramica",
|
||||
"enum_variable_section_security": "6. Sicurezza",
|
||||
"enum_variable_section_fleet": "4. Gestione panoramica",
|
||||
"enum_variable_section_security": "5. Sicurezza",
|
||||
"enum_application_language_english": "Inglese",
|
||||
"enum_application_language_french": "Francese",
|
||||
"enum_application_language_italian": "Italiano",
|
||||
"enum_application_language_spanish": "Spagnolo",
|
||||
"enum_content_type_external_storage": "Archiviazione esterna",
|
||||
"enum_content_type_external_storage_object_label": "Specificare una directory esistente relativi al seguente percorso",
|
||||
"enum_content_type_external_storage_flashdrive_label": "Percorso relativo ad un dispositivo rimovibile",
|
||||
"enum_content_type_composition": "Composizione",
|
||||
"enum_content_type_composition_object_label": "Rapporto di aspetto dello schermo",
|
||||
"enum_content_type_text": "Testo",
|
||||
"enum_content_type_text_object_label": "Testo visualizzato",
|
||||
"enum_content_type_url": "URL",
|
||||
"enum_content_type_video": "Video",
|
||||
"enum_content_type_picture": "Immagine",
|
||||
@ -295,8 +330,8 @@
|
||||
"enum_operating_system_redhat": "RedHat",
|
||||
"enum_operating_system_centos": "CentOS",
|
||||
"enum_operating_system_other": "Altro",
|
||||
"sysinfo_rpi_model": "Raspberry Pi Model",
|
||||
"sysinfo_rpi_model_unknown": "Informazioni Raspberry Pi non disponibili",
|
||||
"sysinfo_device_model": "Modello del dispositivo",
|
||||
"sysinfo_device_model_unknown": "Modello sconosciuto",
|
||||
"sysinfo_storage_free_space": "Spazio libero",
|
||||
"sysinfo_memory_usage": "Memoria usata",
|
||||
"sysinfo_os_version": "OS Version",
|
||||
|
||||
@ -4,5 +4,5 @@ import os
|
||||
from src.Application import Application
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = Application(project_dir=os.path.dirname(__file__))
|
||||
app = Application(application_dir=os.path.dirname(__file__))
|
||||
app.start()
|
||||
|
||||
30
plugins/system/CoreApi/CoreApi.py
Normal file
30
plugins/system/CoreApi/CoreApi.py
Normal file
@ -0,0 +1,30 @@
|
||||
from src.interface.ObPlugin import ObPlugin
|
||||
|
||||
from typing import List, Dict
|
||||
from src.model.entity.Variable import Variable
|
||||
from src.model.enum.HookType import HookType
|
||||
from src.model.hook.HookRegistration import HookRegistration
|
||||
|
||||
|
||||
class CoreApi(ObPlugin):
|
||||
|
||||
def get_version(self) -> str:
|
||||
return '1.0'
|
||||
|
||||
def use_id(self):
|
||||
return 'core_api'
|
||||
|
||||
def use_title(self):
|
||||
return self.translate('plugin_title')
|
||||
|
||||
def use_description(self):
|
||||
return self.translate('plugin_description')
|
||||
|
||||
def use_help_on_activation(self):
|
||||
return self.translate('plugin_help_on_activation')
|
||||
|
||||
def use_variables(self) -> List[Variable]:
|
||||
return []
|
||||
|
||||
def use_hooks_registrations(self) -> List[HookRegistration]:
|
||||
return []
|
||||
375
plugins/system/CoreApi/controller/ContentApiController.py
Normal file
375
plugins/system/CoreApi/controller/ContentApiController.py
Normal file
@ -0,0 +1,375 @@
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
from flask import request, abort, jsonify
|
||||
from flask_restx import Resource, Namespace, fields, reqparse
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.utils import secure_filename
|
||||
from src.model.entity.Content import Content
|
||||
from src.manager.FolderManager import FolderManager
|
||||
from src.model.enum.ContentType import ContentType, ContentInputType
|
||||
from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH
|
||||
from src.interface.ObController import ObController
|
||||
from src.util.utils import str_to_enum
|
||||
from plugins.system.CoreApi.exception.ContentPathMissingException import ContentPathMissingException
|
||||
from plugins.system.CoreApi.exception.ContentNotFoundException import ContentNotFoundException
|
||||
from plugins.system.CoreApi.exception.FolderNotFoundException import FolderNotFoundException
|
||||
from plugins.system.CoreApi.exception.FolderNotEmptyException import FolderNotEmptyException
|
||||
from src.service.WebServer import create_require_api_key_decorator
|
||||
|
||||
|
||||
# Namespace for content operations
|
||||
content_ns = Namespace('contents', description='Operations on contents')
|
||||
|
||||
# Output model for content
|
||||
content_output_model = content_ns.model('ContentOutput', {
|
||||
'id': fields.Integer(readOnly=True, description='Unique identifier of the content'),
|
||||
'name': fields.String(description='Name of the content'),
|
||||
'type': fields.String(description='Type of the content'),
|
||||
'location': fields.String(description='Location of the content'),
|
||||
'folder_id': fields.Integer(description='Folder ID where the content is stored')
|
||||
})
|
||||
|
||||
# Model for folder operations
|
||||
folder_model = content_ns.model('Folder', {
|
||||
'name': fields.String(required=True, description='Name of the folder'),
|
||||
'path': fields.String(required=False, description='Path context (with path starting with /)'),
|
||||
'folder_id': fields.Integer(required=False, description='Path context (with folder id)')
|
||||
})
|
||||
|
||||
# Parser for bulk move operations
|
||||
bulk_move_parser = content_ns.parser()
|
||||
bulk_move_parser.add_argument('entity_ids', type=int, action='append', required=True, help='List of content IDs to move')
|
||||
bulk_move_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
bulk_move_parser.add_argument('folder_id', type=int, required=False, help='Path context (with folder id)')
|
||||
|
||||
# Parser for content add/upload (single file)
|
||||
content_upload_parser = content_ns.parser()
|
||||
content_upload_parser.add_argument('name', type=str, required=True, help='Name of the content')
|
||||
content_upload_parser.add_argument('type', type=str, required=True, help='Type of the content')
|
||||
content_upload_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
content_upload_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)')
|
||||
content_upload_parser.add_argument('location', type=str, required=False, help="Content location (valid for types: {}, {} and {})".format(
|
||||
ContentType.URL.value,
|
||||
ContentType.YOUTUBE.value,
|
||||
ContentType.EXTERNAL_STORAGE.value
|
||||
))
|
||||
content_upload_parser.add_argument('object', type=FileStorage, location='files', required=False, help="Content location (valid for types: {} and {})".format(
|
||||
ContentType.PICTURE.value,
|
||||
ContentType.VIDEO.value
|
||||
))
|
||||
|
||||
# Parser for content add/bulk uploads (multiple files)
|
||||
bulk_upload_parser = content_ns.parser()
|
||||
bulk_upload_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
bulk_upload_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)')
|
||||
bulk_upload_parser.add_argument('object', type=FileStorage, location='files', action='append', required=True, help='Files to be uploaded')
|
||||
|
||||
# Parser for content edit
|
||||
content_edit_parser = content_ns.parser()
|
||||
content_edit_parser.add_argument('name', type=str, required=True, help='Name of the content')
|
||||
|
||||
# Parser for content path context actions
|
||||
path_parser = content_ns.parser()
|
||||
path_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
path_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)')
|
||||
|
||||
# Parser for folder add/edit
|
||||
folder_parser = content_ns.parser()
|
||||
folder_parser.add_argument('name', type=str, required=True, help='Name of the folder')
|
||||
folder_parser.add_argument('path', type=str, required=False, help='Path context (with path starting with /)')
|
||||
folder_parser.add_argument('folder_id', type=str, required=False, help='Path context (with folder id)')
|
||||
|
||||
class ContentApiController(ObController):
|
||||
|
||||
def register(self):
|
||||
self.api().add_namespace(content_ns, path='/api/contents')
|
||||
content_ns.add_resource(self.create_resource(ContentListResource), '/')
|
||||
content_ns.add_resource(self.create_resource(ContentResource), '/<int:content_id>')
|
||||
content_ns.add_resource(self.create_resource(ContentLocationResource), '/location/<int:content_id>')
|
||||
content_ns.add_resource(self.create_resource(ContentBulkUploadResource), '/upload-bulk')
|
||||
content_ns.add_resource(self.create_resource(FolderBulkMoveResource), '/folder/move-bulk')
|
||||
content_ns.add_resource(self.create_resource(FolderResource), '/folder')
|
||||
|
||||
def create_resource(self, resource_class):
|
||||
# Function to inject dependencies into resources
|
||||
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
||||
'_model_store': self._model_store,
|
||||
'_controller': self,
|
||||
'require_api_key': create_require_api_key_decorator(self._web_server)
|
||||
})
|
||||
|
||||
def _get_folder_context(self, data):
|
||||
path = data.get('path', None)
|
||||
folder_id = data.get('folder_id', None)
|
||||
|
||||
if folder_id:
|
||||
folder = self._model_store.folder().get(id=folder_id)
|
||||
|
||||
if not folder:
|
||||
raise FolderNotFoundException()
|
||||
return path, folder
|
||||
|
||||
if not path:
|
||||
raise ContentPathMissingException()
|
||||
|
||||
path = "{}/{}".format(FOLDER_ROOT_PATH, path.strip('/')) if not path.startswith(FOLDER_ROOT_PATH) else path
|
||||
|
||||
folder = self._model_store.folder().get_one_by_path(path=path, entity=FolderEntity.CONTENT)
|
||||
is_root_drive = FolderManager.is_root_drive(path)
|
||||
|
||||
if not folder and not is_root_drive:
|
||||
raise FolderNotFoundException()
|
||||
|
||||
return FOLDER_ROOT_PATH if is_root_drive else path, folder
|
||||
|
||||
def _post_update(self):
|
||||
self._model_store.variable().update_by_name("last_content_update", time.time())
|
||||
|
||||
|
||||
class ContentListResource(Resource):
|
||||
|
||||
@content_ns.expect(path_parser)
|
||||
@content_ns.marshal_list_with(content_output_model)
|
||||
def get(self):
|
||||
"""List all contents"""
|
||||
self.require_api_key()
|
||||
data = path_parser.parse_args()
|
||||
working_folder_path = None
|
||||
working_folder = None
|
||||
folder_id = None
|
||||
|
||||
try:
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
folder_id = data.get('folder_id', 0 if not working_folder else working_folder.id)
|
||||
except FolderNotFoundException:
|
||||
pass
|
||||
except ContentPathMissingException:
|
||||
pass
|
||||
|
||||
contents = self._model_store.content().get_contents(
|
||||
folder_id=folder_id,
|
||||
slide_id=data.get('slide_id', None),
|
||||
)
|
||||
result = [content.to_dict() for content in contents]
|
||||
|
||||
return result
|
||||
|
||||
@content_ns.expect(content_upload_parser)
|
||||
@content_ns.marshal_with(content_output_model, code=201)
|
||||
def post(self):
|
||||
"""Add new content"""
|
||||
self.require_api_key()
|
||||
data = content_upload_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
location = data.get('location', None)
|
||||
content_type = None
|
||||
|
||||
# Handle content type conversion
|
||||
try:
|
||||
content_type = str_to_enum(data.get('type'), ContentType)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
# Handle file upload
|
||||
file = data.get('object', None)
|
||||
|
||||
if ContentType.get_input(content_type) == ContentInputType.UPLOAD:
|
||||
if not file:
|
||||
abort(400, description="File is required")
|
||||
|
||||
content = self._model_store.content().add_form_raw(
|
||||
name=data.get('name'),
|
||||
type=content_type,
|
||||
request_files=file,
|
||||
upload_dir=self._controller._app.config['UPLOAD_FOLDER'],
|
||||
location=location,
|
||||
folder_id=working_folder.id if working_folder else None
|
||||
)
|
||||
|
||||
if not content:
|
||||
abort(400, description="Failed to add content")
|
||||
|
||||
return content.to_dict(), 201
|
||||
|
||||
|
||||
class ContentResource(Resource):
|
||||
|
||||
@content_ns.marshal_with(content_output_model)
|
||||
def get(self, content_id: int):
|
||||
"""Get content by ID"""
|
||||
self.require_api_key()
|
||||
content = self._model_store.content().get(content_id)
|
||||
if not content:
|
||||
raise ContentNotFoundException()
|
||||
|
||||
return content.to_dict()
|
||||
|
||||
@content_ns.expect(content_edit_parser)
|
||||
@content_ns.marshal_with(content_output_model)
|
||||
def put(self, content_id: int):
|
||||
"""Update existing content"""
|
||||
self.require_api_key()
|
||||
data = content_edit_parser.parse_args()
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
raise ContentNotFoundException()
|
||||
|
||||
if 'name' not in data:
|
||||
abort(400, description="Name is required")
|
||||
|
||||
content = self._model_store.content().update_form(
|
||||
id=content.id,
|
||||
name=data.get('name'),
|
||||
)
|
||||
|
||||
self._controller._post_update()
|
||||
|
||||
return content.to_dict()
|
||||
|
||||
def delete(self, content_id: int):
|
||||
"""Delete content"""
|
||||
self.require_api_key()
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
raise ContentNotFoundException()
|
||||
|
||||
if self._model_store.slide().count_slides_for_content(content.id) > 0:
|
||||
abort(400, description="Content is referenced in slides")
|
||||
|
||||
self._model_store.content().delete(content.id)
|
||||
self._controller._post_update()
|
||||
|
||||
return {'status': 'ok'}, 204
|
||||
|
||||
|
||||
class ContentLocationResource(Resource):
|
||||
|
||||
def get(self, content_id: int):
|
||||
"""Get content location by ID"""
|
||||
self.require_api_key()
|
||||
content = self._model_store.content().get(content_id)
|
||||
|
||||
if not content:
|
||||
raise ContentNotFoundException()
|
||||
|
||||
content_location = self._model_store.content().resolve_content_location(content)
|
||||
|
||||
return {'location': content_location}
|
||||
|
||||
|
||||
class ContentBulkUploadResource(Resource):
|
||||
|
||||
@content_ns.expect(bulk_upload_parser)
|
||||
def post(self):
|
||||
"""Upload multiple content files"""
|
||||
self.require_api_key()
|
||||
data = bulk_upload_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
for file in data.get('object'):
|
||||
content_type = ContentType.guess_content_type_file(file.filename)
|
||||
name = file.filename.rsplit('.', 1)[0]
|
||||
|
||||
if content_type:
|
||||
self._model_store.content().add_form_raw(
|
||||
name=name,
|
||||
type=content_type,
|
||||
request_files=file,
|
||||
upload_dir=self._controller._app.config['UPLOAD_FOLDER'],
|
||||
folder_id=working_folder.id if working_folder else None
|
||||
)
|
||||
|
||||
return {'status': 'ok'}, 201
|
||||
|
||||
|
||||
class FolderBulkMoveResource(Resource):
|
||||
|
||||
@content_ns.expect(bulk_move_parser)
|
||||
def post(self):
|
||||
"""Move multiple content to another folder"""
|
||||
self.require_api_key()
|
||||
data = bulk_move_parser.parse_args()
|
||||
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
if 'entity_ids' not in data:
|
||||
abort(400, description="Content IDs are required under 'entity_ids' field")
|
||||
|
||||
entity_ids = data.get('entity_ids')
|
||||
|
||||
for entity_id in entity_ids:
|
||||
self._model_store.folder().move_to_folder(
|
||||
entity_id=entity_id,
|
||||
folder_id=working_folder.id if working_folder else None,
|
||||
entity_is_folder=False,
|
||||
entity=FolderEntity.CONTENT
|
||||
)
|
||||
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
class FolderResource(Resource):
|
||||
|
||||
@content_ns.expect(folder_parser)
|
||||
@content_ns.marshal_with(folder_model, code=201)
|
||||
def post(self):
|
||||
"""Add a new folder"""
|
||||
self.require_api_key()
|
||||
data = folder_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
if 'name' not in data:
|
||||
abort(400, description="Name is required")
|
||||
|
||||
folder = self._model_store.folder().add_folder(
|
||||
entity=FolderEntity.CONTENT,
|
||||
name=data.get('name'),
|
||||
working_folder_path=working_folder_path
|
||||
)
|
||||
|
||||
return folder.to_dict(), 201
|
||||
|
||||
@content_ns.expect(path_parser)
|
||||
def delete(self):
|
||||
"""Delete a folder"""
|
||||
self.require_api_key()
|
||||
data = path_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
if not working_folder:
|
||||
abort(400, description="You can't delete this folder")
|
||||
|
||||
content_counter = self._model_store.content().count_contents_for_folder(working_folder.id)
|
||||
folder_counter = self._model_store.folder().count_subfolders_for_folder(working_folder.id)
|
||||
|
||||
if content_counter > 0 or folder_counter:
|
||||
raise FolderNotEmptyException()
|
||||
|
||||
self._model_store.folder().delete(id=working_folder.id)
|
||||
self._controller._post_update()
|
||||
|
||||
return {'status': 'ok'}, 204
|
||||
|
||||
@content_ns.expect(folder_parser)
|
||||
def put(self):
|
||||
"""Update a folder"""
|
||||
self.require_api_key()
|
||||
data = folder_parser.parse_args()
|
||||
working_folder_path, working_folder = self._controller._get_folder_context(data)
|
||||
|
||||
if 'name' not in data:
|
||||
abort(400, description="Name is required")
|
||||
|
||||
if not working_folder:
|
||||
abort(400, description="You can't update this folder")
|
||||
|
||||
self._model_store.folder().rename_folder(
|
||||
folder_id=working_folder.id,
|
||||
name=data.get('name')
|
||||
)
|
||||
|
||||
return {'status': 'ok'}
|
||||
161
plugins/system/CoreApi/controller/PlaylistApiController.py
Normal file
161
plugins/system/CoreApi/controller/PlaylistApiController.py
Normal file
@ -0,0 +1,161 @@
|
||||
from flask import request, abort, jsonify
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
from src.model.entity.Playlist import Playlist
|
||||
from src.interface.ObController import ObController
|
||||
from src.util.utils import str_to_bool
|
||||
from src.service.WebServer import create_require_api_key_decorator
|
||||
|
||||
|
||||
# Namespace for playlists operations
|
||||
playlist_ns = Namespace('playlists', description='Operations on playlist')
|
||||
|
||||
# Output model for a playlist
|
||||
playlist_output_model = playlist_ns.model('PlaylistOutput', {
|
||||
'id': fields.Integer(readOnly=True, description='The unique identifier of a playlist'),
|
||||
'name': fields.String(required=True, description='The playlist name'),
|
||||
'enabled': fields.Boolean(description='Is the playlist enabled?'),
|
||||
'time_sync': fields.Boolean(description='Is time synchronization enabled?')
|
||||
})
|
||||
|
||||
# Parser for playlist attributes (add)
|
||||
playlist_parser = playlist_ns.parser()
|
||||
playlist_parser.add_argument('name', type=str, required=True, help='The playlist name')
|
||||
playlist_parser.add_argument('enabled', type=str_to_bool, default=None, help='Is the playlist enabled?')
|
||||
playlist_parser.add_argument('time_sync', type=str_to_bool, default=None, help='Is time synchronization enabled for slideshow?')
|
||||
|
||||
# Parser for playlist attributes (update)
|
||||
playlist_edit_parser = playlist_parser.copy()
|
||||
playlist_edit_parser.replace_argument('name', type=str, required=False, help='The playlist name')
|
||||
|
||||
|
||||
class PlaylistApiController(ObController):
|
||||
|
||||
def register(self):
|
||||
self.api().add_namespace(playlist_ns, path='/api/playlists')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistResource), '/<int:playlist_id>')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistListResource), '/')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistSlidesResource), '/<int:playlist_id>/slides')
|
||||
playlist_ns.add_resource(self.create_resource(PlaylistNotificationsResource), '/<int:playlist_id>/notifications')
|
||||
|
||||
def create_resource(self, resource_class):
|
||||
# Function to inject dependencies into resources
|
||||
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
||||
'_model_store': self._model_store,
|
||||
'_controller': self,
|
||||
'require_api_key': create_require_api_key_decorator(self._web_server)
|
||||
})
|
||||
|
||||
|
||||
class PlaylistListResource(Resource):
|
||||
|
||||
@playlist_ns.marshal_list_with(playlist_output_model)
|
||||
def get(self):
|
||||
"""List all playlists"""
|
||||
self.require_api_key()
|
||||
playlists = self._model_store.playlist().get_all(sort="created_at", ascending=True)
|
||||
result = [playlist.to_dict() for playlist in playlists]
|
||||
return result
|
||||
|
||||
@playlist_ns.expect(playlist_parser)
|
||||
@playlist_ns.marshal_with(playlist_output_model, code=201)
|
||||
def post(self):
|
||||
"""Create a new playlist"""
|
||||
self.require_api_key()
|
||||
data = playlist_parser.parse_args()
|
||||
|
||||
if not data.get('name'):
|
||||
abort(400, description="Invalid input")
|
||||
|
||||
playlist = Playlist(
|
||||
name=data.get('name'),
|
||||
enabled=data.get('enabled') if data.get('enabled') is not None else True,
|
||||
time_sync=data.get('time_sync') if data.get('time_sync') is not None else False,
|
||||
)
|
||||
|
||||
try:
|
||||
playlist = self._model_store.playlist().add_form(playlist)
|
||||
except Exception as e:
|
||||
abort(409, description=str(e))
|
||||
|
||||
return playlist.to_dict(), 201
|
||||
|
||||
|
||||
class PlaylistResource(Resource):
|
||||
|
||||
@playlist_ns.marshal_with(playlist_output_model)
|
||||
def get(self, playlist_id):
|
||||
"""Get a playlist by its ID"""
|
||||
self.require_api_key()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
return playlist.to_dict()
|
||||
|
||||
@playlist_ns.expect(playlist_edit_parser)
|
||||
@playlist_ns.marshal_with(playlist_output_model)
|
||||
def put(self, playlist_id):
|
||||
"""Update an existing playlist"""
|
||||
self.require_api_key()
|
||||
data = playlist_edit_parser.parse_args()
|
||||
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
self._model_store.playlist().update_form(
|
||||
id=playlist_id,
|
||||
name=data.get('name', playlist.name),
|
||||
time_sync=data.get('time_sync', playlist.time_sync),
|
||||
enabled=data.get('enabled', playlist.enabled)
|
||||
)
|
||||
updated_playlist = self._model_store.playlist().get(playlist_id)
|
||||
return updated_playlist.to_dict()
|
||||
|
||||
def delete(self, playlist_id):
|
||||
"""Delete a playlist"""
|
||||
self.require_api_key()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
if self._model_store.slide().count_slides_for_playlist(playlist_id) > 0:
|
||||
abort(400, description="Playlist cannot be deleted because it has slides")
|
||||
|
||||
if self._model_store.node_player_group().count_node_player_groups_for_playlist(playlist_id) > 0:
|
||||
abort(400, description="Playlist cannot be deleted because it is associated with node player groups")
|
||||
|
||||
self._model_store.playlist().delete(playlist_id)
|
||||
return '', 204
|
||||
|
||||
|
||||
class PlaylistSlidesResource(Resource):
|
||||
|
||||
def get(self, playlist_id):
|
||||
"""Get slides associated with a playlist"""
|
||||
self.require_api_key()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
slides = self._model_store.slide().get_slides(is_notification=False, playlist_id=playlist_id)
|
||||
|
||||
result = [slide.to_dict() for slide in slides]
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
class PlaylistNotificationsResource(Resource):
|
||||
|
||||
def get(self, playlist_id):
|
||||
"""Get notifications associated with a playlist"""
|
||||
self.require_api_key()
|
||||
playlist = self._model_store.playlist().get(playlist_id)
|
||||
|
||||
if not playlist:
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
slides = self._model_store.slide().get_slides(is_notification=True, playlist_id=playlist_id)
|
||||
|
||||
result = [slide.to_dict() for slide in slides]
|
||||
return jsonify(result)
|
||||
322
plugins/system/CoreApi/controller/SlideApiController.py
Normal file
322
plugins/system/CoreApi/controller/SlideApiController.py
Normal file
@ -0,0 +1,322 @@
|
||||
import time
|
||||
|
||||
from flask import request, abort, jsonify
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
from src.model.entity.Slide import Slide
|
||||
from src.interface.ObController import ObController
|
||||
from src.util.utils import str_datetime_to_cron, str_weekdaytime_to_cron, str_to_bool
|
||||
from src.service.WebServer import create_require_api_key_decorator
|
||||
|
||||
# Namespace for slide operations
|
||||
slide_ns = Namespace('slides', description='Operations on slides')
|
||||
|
||||
# Output model for a slide
|
||||
slide_output_model = slide_ns.model('SlideOutput', {
|
||||
'id': fields.Integer(readOnly=True, description='The unique identifier of a slide'),
|
||||
'content_id': fields.Integer(description='The content ID for the slide'),
|
||||
'playlist_id': fields.Integer(description='The playlist ID to which the slide belongs'),
|
||||
'enabled': fields.Boolean(description='Is the slide enabled?'),
|
||||
'delegate_duration': fields.Boolean(description='Should the duration be delegated?'),
|
||||
'duration': fields.Integer(description='Duration of the slide'),
|
||||
'position': fields.Integer(description='Position of the slide'),
|
||||
'is_notification': fields.Boolean(description='Is the slide a notification?'),
|
||||
'cron_schedule': fields.String(description='Cron expression for scheduling start'),
|
||||
'cron_schedule_end': fields.String(description='Cron expression for scheduling end'),
|
||||
})
|
||||
|
||||
# Input model for updating slide positions
|
||||
positions_model = slide_ns.model('SlidePositions', {
|
||||
'positions': fields.Raw(required=True, description='A dictionary where keys are slide IDs and values are their new positions')
|
||||
})
|
||||
|
||||
# Parser for basic slide attributes
|
||||
slide_base_parser = slide_ns.parser()
|
||||
slide_base_parser.add_argument('content_id', type=int, required=True, help='The content ID for the slide')
|
||||
slide_base_parser.add_argument('playlist_id', type=int, required=True, help='The playlist ID to which the slide belongs')
|
||||
slide_base_parser.add_argument('enabled', type=str_to_bool, default=None, help='Is the slide enabled?')
|
||||
slide_base_parser.add_argument('duration', type=int, default=3, help='Duration of the slide')
|
||||
slide_base_parser.add_argument('position', type=int, default=999, help='Position of the slide')
|
||||
|
||||
# Parser for slide attributes (add)
|
||||
slide_parser = slide_base_parser.copy()
|
||||
slide_parser.add_argument('scheduling', type=str, required=True, help='Scheduling type: loop, datetime or inweek')
|
||||
slide_parser.add_argument('delegate_duration', type=str_to_bool, default=None, help='Should the duration be delegated to video\'s duration?')
|
||||
slide_parser.add_argument('datetime_start', type=str, required=False, help='Start datetime for scheduling (format: Y-m-d H:M)')
|
||||
slide_parser.add_argument('datetime_end', type=str, required=False, help='End datetime for scheduling (format: Y-m-d H:M)')
|
||||
slide_parser.add_argument('day_start', type=int, required=False, help='Start day for inweek scheduling (format: 1 for Monday to 7 for Sunday)')
|
||||
slide_parser.add_argument('time_start', type=str, required=False, help='Start time for inweek scheduling (format: H:M)')
|
||||
slide_parser.add_argument('day_end', type=int, required=False, help='End day for inweek scheduling (format: 1 for Monday to 7 for Sunday)')
|
||||
slide_parser.add_argument('time_end', type=str, required=False, help='End time for inweek scheduling (format: H:M)')
|
||||
|
||||
# Parser for slide notification attributes (add)
|
||||
slide_notification_parser = slide_base_parser.copy()
|
||||
slide_notification_parser.add_argument('scheduling', type=str, required=True, help='Scheduling type: datetime or cron')
|
||||
slide_notification_parser.add_argument('datetime_start', type=str, required=False, help='Start datetime for notification scheduling (format: Y-m-d H:M)')
|
||||
slide_notification_parser.add_argument('datetime_end', type=str, required=False, help='End datetime for notification scheduling (format: Y-m-d H:M)')
|
||||
slide_notification_parser.add_argument('cron_start', type=str, required=False, help='Cron expression for notification scheduling start (format: * * * * * * *)')
|
||||
slide_notification_parser.add_argument('cron_end', type=str, required=False, help='Cron expression for notification scheduling end (format: * * * * * * *)')
|
||||
|
||||
# Parser for slide attributes (update)
|
||||
slide_edit_parser = slide_parser.copy()
|
||||
slide_edit_parser.replace_argument('scheduling', type=str, required=False, help='Scheduling type: loop, datetime, or inweek')
|
||||
slide_edit_parser.replace_argument('content_id', type=int, required=False, help='The content ID for the slide')
|
||||
slide_edit_parser.replace_argument('playlist_id', type=int, required=False, help='The playlist ID to which the slide belongs')
|
||||
|
||||
# Parser for slide notification attributes (update)
|
||||
slide_notification_edit_parser = slide_notification_parser.copy()
|
||||
slide_notification_edit_parser.replace_argument('scheduling', type=str, required=False, help='Scheduling type: datetime or cron')
|
||||
slide_notification_edit_parser.replace_argument('content_id', type=int, required=False, help='The content ID for the slide')
|
||||
slide_notification_edit_parser.replace_argument('playlist_id', type=int, required=False, help='The playlist ID to which the slide belongs')
|
||||
|
||||
|
||||
class SlideApiController(ObController):
|
||||
|
||||
def register(self):
|
||||
self.api().add_namespace(slide_ns, path='/api/slides')
|
||||
slide_ns.add_resource(self.create_resource(SlideNotificationResource), '/notifications/<int:slide_id>')
|
||||
slide_ns.add_resource(self.create_resource(SlideResource), '/<int:slide_id>')
|
||||
slide_ns.add_resource(self.create_resource(SlideAddResource), '/')
|
||||
slide_ns.add_resource(self.create_resource(SlideAddNotificationResource), '/notifications')
|
||||
slide_ns.add_resource(self.create_resource(SlidePositionResource), '/positions')
|
||||
|
||||
def create_resource(self, resource_class):
|
||||
# Function to inject dependencies into resources
|
||||
return type(f'{resource_class.__name__}WithDependencies', (resource_class,), {
|
||||
'_model_store': self._model_store,
|
||||
'_controller': self,
|
||||
'require_api_key': create_require_api_key_decorator(self._web_server)
|
||||
})
|
||||
|
||||
def _add_slide_or_notification(self, data, is_notification=False):
|
||||
if not data or 'content_id' not in data:
|
||||
abort(400, description="Valid Content ID is required")
|
||||
|
||||
if not self._model_store.content().get(data.get('content_id')):
|
||||
abort(404, description="Content not found")
|
||||
|
||||
if not data or 'playlist_id' not in data:
|
||||
abort(400, description="Valid Playlist ID is required")
|
||||
|
||||
if not self._model_store.playlist().get(data.get('playlist_id')):
|
||||
abort(404, description="Playlist not found")
|
||||
|
||||
cron_schedule_start, cron_schedule_end = self._resolve_scheduling(data, is_notification=is_notification)
|
||||
|
||||
slide = Slide(
|
||||
content_id=data.get('content_id'),
|
||||
enabled=data.get('enabled') if data.get('enabled') is not None else True,
|
||||
delegate_duration=data.get('delegate_duration') if data.get('delegate_duration') is not None else False,
|
||||
duration=data.get('duration', 3),
|
||||
position=data.get('position', 999),
|
||||
is_notification=is_notification,
|
||||
playlist_id=data.get('playlist_id', None),
|
||||
cron_schedule=cron_schedule_start,
|
||||
cron_schedule_end=cron_schedule_end
|
||||
)
|
||||
|
||||
slide = self._model_store.slide().add_form(slide)
|
||||
self._post_update()
|
||||
|
||||
return slide.to_dict(), 201
|
||||
|
||||
def _resolve_scheduling(self, data, is_notification=False):
|
||||
try:
|
||||
return self._resolve_scheduling_for_notification(data) if is_notification else self._resolve_scheduling_for_slide(data)
|
||||
except ValueError as ve:
|
||||
abort(400, description=str(ve))
|
||||
|
||||
def _resolve_scheduling_for_slide(self, data):
|
||||
scheduling = data.get('scheduling', 'loop')
|
||||
cron_schedule_start = None
|
||||
cron_schedule_end = None
|
||||
|
||||
if scheduling == 'loop':
|
||||
pass
|
||||
elif scheduling == 'datetime':
|
||||
datetime_start = data.get('datetime_start')
|
||||
datetime_end = data.get('datetime_end')
|
||||
|
||||
if not datetime_start:
|
||||
abort(400, description="Field datetime_start is required for scheduling='datetime'")
|
||||
|
||||
cron_schedule_start = str_datetime_to_cron(datetime_str=datetime_start)
|
||||
|
||||
if datetime_end:
|
||||
cron_schedule_end = str_datetime_to_cron(datetime_str=datetime_end)
|
||||
elif scheduling == 'inweek':
|
||||
day_start = data.get('day_start')
|
||||
time_start = data.get('time_start')
|
||||
day_end = data.get('day_end')
|
||||
time_end = data.get('time_end')
|
||||
|
||||
if not (day_start and time_start and day_end and time_end):
|
||||
abort(400, description="day_start, time_start, day_end, and time_end are required for scheduling='inweek'")
|
||||
cron_schedule_start = str_weekdaytime_to_cron(weekday=int(day_start), time_str=time_start)
|
||||
cron_schedule_end = str_weekdaytime_to_cron(weekday=int(day_end), time_str=time_end)
|
||||
else:
|
||||
abort(400, description="Invalid value for slide scheduling. Expected 'loop', 'datetime', or 'inweek'.")
|
||||
|
||||
return cron_schedule_start, cron_schedule_end
|
||||
|
||||
def _resolve_scheduling_for_notification(self, data):
|
||||
scheduling = data.get('scheduling', 'datetime')
|
||||
cron_schedule_start = None
|
||||
cron_schedule_end = None
|
||||
|
||||
if scheduling == 'datetime':
|
||||
datetime_start = data.get('datetime_start')
|
||||
datetime_end = data.get('datetime_end')
|
||||
|
||||
if not datetime_start:
|
||||
abort(400, description="Field datetime_start is required for scheduling='datetime'")
|
||||
|
||||
cron_schedule_start = str_datetime_to_cron(datetime_str=datetime_start)
|
||||
|
||||
if datetime_end:
|
||||
cron_schedule_end = str_datetime_to_cron(datetime_str=datetime_end)
|
||||
elif scheduling == 'cron':
|
||||
cron_schedule_start = data.get('cron_start')
|
||||
|
||||
if not cron_schedule_start:
|
||||
abort(400, description="Field cron_start is required for scheduling='cron'")
|
||||
else:
|
||||
abort(400, description="Invalid value for notification scheduling. Expected 'datetime' or 'cron'.")
|
||||
|
||||
return cron_schedule_start, cron_schedule_end
|
||||
|
||||
def _post_update(self):
|
||||
self._model_store.variable().update_by_name("last_slide_update", time.time())
|
||||
|
||||
|
||||
class SlideAddResource(Resource):
|
||||
|
||||
@slide_ns.expect(slide_parser)
|
||||
@slide_ns.marshal_with(slide_output_model, code=201)
|
||||
def post(self):
|
||||
"""Add a new slide"""
|
||||
self.require_api_key()
|
||||
data = slide_parser.parse_args()
|
||||
return self._controller._add_slide_or_notification(data, is_notification=False)
|
||||
|
||||
|
||||
class SlideAddNotificationResource(Resource):
|
||||
|
||||
@slide_ns.expect(slide_notification_parser)
|
||||
@slide_ns.marshal_with(slide_output_model, code=201)
|
||||
def post(self):
|
||||
"""Add a new slide notification"""
|
||||
self.require_api_key()
|
||||
data = slide_notification_parser.parse_args()
|
||||
return self._controller._add_slide_or_notification(data, is_notification=True)
|
||||
|
||||
|
||||
class SlideResource(Resource):
|
||||
|
||||
@slide_ns.marshal_with(slide_output_model)
|
||||
def get(self, slide_id):
|
||||
"""Get a slide by its ID"""
|
||||
self.require_api_key()
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
return slide.to_dict()
|
||||
|
||||
@slide_ns.expect(slide_edit_parser)
|
||||
@slide_ns.marshal_with(slide_output_model)
|
||||
def put(self, slide_id):
|
||||
"""Edit an existing slide"""
|
||||
self.require_api_key()
|
||||
data = slide_edit_parser.parse_args()
|
||||
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
cron_schedule_start = slide.cron_schedule
|
||||
cron_schedule_end = slide.cron_schedule_end
|
||||
|
||||
if data.get('scheduling'):
|
||||
cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification)
|
||||
|
||||
self._model_store.slide().update_form(
|
||||
id=slide_id,
|
||||
content_id=data.get('content_id', slide.content_id),
|
||||
enabled=data.get('enabled', slide.enabled),
|
||||
position=data.get('position', slide.position),
|
||||
duration=data.get('duration', slide.duration),
|
||||
cron_schedule=cron_schedule_start,
|
||||
cron_schedule_end=cron_schedule_end
|
||||
)
|
||||
self._controller._post_update()
|
||||
|
||||
updated_slide = self._model_store.slide().get(slide_id)
|
||||
return updated_slide.to_dict()
|
||||
|
||||
def delete(self, slide_id):
|
||||
"""Delete a slide"""
|
||||
self.require_api_key()
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
self._model_store.slide().delete(slide_id)
|
||||
self._controller._post_update()
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
class SlideNotificationResource(Resource):
|
||||
|
||||
@slide_ns.expect(slide_notification_edit_parser)
|
||||
@slide_ns.marshal_with(slide_output_model)
|
||||
def put(self, slide_id):
|
||||
"""Edit an existing slide notification"""
|
||||
self.require_api_key()
|
||||
data = slide_notification_edit_parser.parse_args()
|
||||
|
||||
slide = self._model_store.slide().get(slide_id)
|
||||
if not slide:
|
||||
abort(404, description="Slide not found")
|
||||
|
||||
cron_schedule_start = slide.cron_schedule
|
||||
cron_schedule_end = slide.cron_schedule_end
|
||||
|
||||
if data.get('scheduling'):
|
||||
cron_schedule_start, cron_schedule_end = self._controller._resolve_scheduling(data, is_notification=slide.is_notification)
|
||||
|
||||
self._model_store.slide().update_form(
|
||||
id=slide_id,
|
||||
content_id=data.get('content_id', slide.content_id),
|
||||
enabled=data.get('enabled', slide.enabled),
|
||||
position=data.get('position', slide.position),
|
||||
delegate_duration=data.get('delegate_duration', slide.delegate_duration),
|
||||
duration=data.get('duration', slide.duration),
|
||||
cron_schedule=cron_schedule_start,
|
||||
cron_schedule_end=cron_schedule_end
|
||||
)
|
||||
self._controller._post_update()
|
||||
|
||||
updated_slide = self._model_store.slide().get(slide_id)
|
||||
return updated_slide.to_dict()
|
||||
|
||||
|
||||
class SlidePositionResource(Resource):
|
||||
|
||||
@slide_ns.expect(positions_model)
|
||||
def post(self):
|
||||
"""Update positions of multiple slides"""
|
||||
self.require_api_key()
|
||||
data = request.get_json()
|
||||
positions = data.get('positions', None) if data else None
|
||||
|
||||
if not positions:
|
||||
abort(400, description="Positions data are required")
|
||||
|
||||
# Ensure the input is a dictionary with integer keys and values
|
||||
if not isinstance(data, dict) or not all(isinstance(k, str) and isinstance(v, int) for k, v in positions.items()):
|
||||
abort(400, description="Input must be a dictionary with string keys as slide IDs and integer values as positions")
|
||||
|
||||
self._model_store.slide().update_positions(positions)
|
||||
self._controller._post_update()
|
||||
return jsonify({'status': 'ok'})
|
||||
@ -0,0 +1,6 @@
|
||||
from src.exceptions.HttpClientException import HttpClientException
|
||||
|
||||
|
||||
class ContentNotFoundException(HttpClientException):
|
||||
code = 404
|
||||
description = "Content not found"
|
||||
@ -0,0 +1,6 @@
|
||||
from src.exceptions.HttpClientException import HttpClientException
|
||||
|
||||
|
||||
class ContentPathMissingException(HttpClientException):
|
||||
code = 400
|
||||
description = "Path is required"
|
||||
@ -0,0 +1,6 @@
|
||||
from src.exceptions.HttpClientException import HttpClientException
|
||||
|
||||
|
||||
class FolderNotEmptyException(HttpClientException):
|
||||
code = 400
|
||||
description = "Folder is not empty"
|
||||
@ -0,0 +1,6 @@
|
||||
from src.exceptions.HttpClientException import HttpClientException
|
||||
|
||||
|
||||
class FolderNotFoundException(HttpClientException):
|
||||
code = 404
|
||||
description = "Folder not found"
|
||||
5
plugins/system/CoreApi/lang/en.json
Normal file
5
plugins/system/CoreApi/lang/en.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Core API",
|
||||
"plugin_description": "Adds api feature wrapping core features",
|
||||
"plugin_help_on_activation": "Documentation will be available on the /api page"
|
||||
}
|
||||
5
plugins/system/CoreApi/lang/es.json
Normal file
5
plugins/system/CoreApi/lang/es.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Core API",
|
||||
"plugin_description": "Agrega características de API que envuelven las características principales",
|
||||
"plugin_help_on_activation": "La documentación estará disponible en la página /api"
|
||||
}
|
||||
5
plugins/system/CoreApi/lang/fr.json
Normal file
5
plugins/system/CoreApi/lang/fr.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Core API",
|
||||
"plugin_description": "Ajoute des fonctionnalités d'API englobant les fonctionnalités principales",
|
||||
"plugin_help_on_activation": "La documentation sera disponible sur la page /api"
|
||||
}
|
||||
5
plugins/system/CoreApi/lang/it.json
Normal file
5
plugins/system/CoreApi/lang/it.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Core API",
|
||||
"plugin_description": "Aggiunge funzionalità API che racchiudono le funzionalità di base",
|
||||
"plugin_help_on_activation": "La documentazione sarà disponibile nella pagina /api"
|
||||
}
|
||||
@ -10,8 +10,11 @@ from src.util.utils import am_i_in_docker
|
||||
|
||||
class GitUpdater(ObPlugin):
|
||||
|
||||
def get_version(self) -> str:
|
||||
return '1.0'
|
||||
|
||||
def use_id(self):
|
||||
return 'git_updater'
|
||||
return 'core_updater'
|
||||
|
||||
def use_title(self):
|
||||
return self.translate('plugin_title')
|
||||
@ -19,6 +22,9 @@ class GitUpdater(ObPlugin):
|
||||
def use_description(self):
|
||||
return self.translate('plugin_description')
|
||||
|
||||
def use_help_on_activation(self):
|
||||
return None
|
||||
|
||||
def use_variables(self) -> List[Variable]:
|
||||
return []
|
||||
|
||||
@ -10,10 +10,10 @@ from src.util.utils import run_system_command, sudo_run_system_command, get_work
|
||||
from src.Application import Application
|
||||
|
||||
|
||||
class GitUpdaterController(ObController):
|
||||
class CoreUpdaterController(ObController):
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/git-updater/update/now', 'git_updater_update_now', self._auth(self.update_now), methods=['GET'])
|
||||
self._app.add_url_rule('/core-updater/update/now', 'core_updater_update_now', self._auth(self.update_now), methods=['GET'])
|
||||
|
||||
def update_now(self):
|
||||
debug = self._model_store.config().map().get('debug')
|
||||
@ -43,6 +43,7 @@ class GitUpdaterController(ObController):
|
||||
elif os_name == "darwin":
|
||||
logging.warn('Git Updater doesn\'t supports macos dependency manager, install system dependencies manually with homebrew')
|
||||
|
||||
run_system_command(['git', 'config', '--global', '--add', 'safe.directory', get_working_directory()])
|
||||
run_system_command(['git', '-C', get_working_directory(), 'stash'])
|
||||
run_system_command(['git', '-C', get_working_directory(), 'checkout', 'master'])
|
||||
run_system_command(['git', '-C', get_working_directory(), 'pull'])
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Git Updater Button",
|
||||
"plugin_description": "Adds an update button (only for system wide installations)",
|
||||
"plugin_title": "Core Updater Button",
|
||||
"plugin_description": "Adds an update button (only for system-wide installations)",
|
||||
"button_update": "Update"
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Botón de Actualización de Git",
|
||||
"plugin_title": "Core Updater Button",
|
||||
"plugin_description": "Añade un botón de actualización (solo para instalaciones a nivel del sistema)",
|
||||
"button_update": "Actualizar"
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Bouton de mise à jour",
|
||||
"plugin_title": "Core Updater Button",
|
||||
"plugin_description": "Ajoute un bouton de mise à jour (seulement pour les installations système)",
|
||||
"button_update": "Mettre à jour"
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Pulsante di aggiornamento",
|
||||
"plugin_title": "Core Updater Button",
|
||||
"plugin_description": "Aggiunge un pulsante di aggiornamento (solo per installazioni di sistema)",
|
||||
"button_update": "Aggiorna"
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{% if not am_i_in_docker() %}
|
||||
<a href="{{ url_for('core_updater_update_now') }}" class="btn sysinfo-update protected"><i class="fa fa-cloud-arrow-down icon-left"></i>{{ l.core_updater_button_update }}</a>
|
||||
{% endif %}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
{% if not am_i_in_docker() %}
|
||||
<a href="{{ url_for('git_updater_update_now') }}" class="btn sysinfo-update protected"><i class="fa fa-cloud-arrow-down icon-left"></i>{{ l.git_updater_button_update }}</a>
|
||||
{% endif %}
|
||||
|
||||
35
plugins/user/Dashboard/Dashboard.py
Normal file
35
plugins/user/Dashboard/Dashboard.py
Normal file
@ -0,0 +1,35 @@
|
||||
from src.interface.ObPlugin import ObPlugin
|
||||
|
||||
from typing import List, Dict
|
||||
from src.model.entity.Variable import Variable
|
||||
from src.model.enum.HookType import HookType
|
||||
from src.model.hook.HookRegistration import HookRegistration
|
||||
|
||||
|
||||
class Dashboard(ObPlugin):
|
||||
|
||||
def get_version(self) -> str:
|
||||
return '1.0'
|
||||
|
||||
def use_id(self):
|
||||
return 'dashboard'
|
||||
|
||||
def use_title(self):
|
||||
return self.translate('plugin_title')
|
||||
|
||||
def use_description(self):
|
||||
return self.translate('plugin_description')
|
||||
|
||||
def use_help_on_activation(self):
|
||||
return None
|
||||
|
||||
def use_variables(self) -> List[Variable]:
|
||||
return []
|
||||
|
||||
def use_hooks_registrations(self) -> List[HookRegistration]:
|
||||
return [
|
||||
super().add_functional_hook_registration(hook=HookType.H_ROOT_NAV_ELEMENT_START, priority=10, function=self.hook_navigation),
|
||||
]
|
||||
|
||||
def hook_navigation(self) -> str:
|
||||
return self.render_view('@hook_navigation.jinja.html')
|
||||
15
plugins/user/Dashboard/controller/DashboardController.py
Normal file
15
plugins/user/Dashboard/controller/DashboardController.py
Normal file
@ -0,0 +1,15 @@
|
||||
from flask import Flask, render_template
|
||||
|
||||
from src.interface.ObController import ObController
|
||||
|
||||
|
||||
class DashboardController(ObController):
|
||||
|
||||
def register(self):
|
||||
self._app.add_url_rule('/dashboard', 'dashboard', self._auth(self.dashboard), methods=['GET'])
|
||||
|
||||
def dashboard(self):
|
||||
return self.render_view(
|
||||
'@dashboard.jinja.html',
|
||||
count_players=len(self._model_store.node_player().get_node_players())
|
||||
)
|
||||
5
plugins/user/Dashboard/lang/en.json
Normal file
5
plugins/user/Dashboard/lang/en.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Dashboard",
|
||||
"plugin_description": "Adds a dashboard reachable from navigation (has no use - developer demo plugin only)",
|
||||
"menu_title": "Dashboard"
|
||||
}
|
||||
5
plugins/user/Dashboard/lang/es.json
Normal file
5
plugins/user/Dashboard/lang/es.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Panel de control",
|
||||
"plugin_description": "Agrega un panel de control accesible desde la navegación (no utilizado, solo complemento de demostración para desarrolladores)",
|
||||
"menu_title": "Panel de control"
|
||||
}
|
||||
5
plugins/user/Dashboard/lang/fr.json
Normal file
5
plugins/user/Dashboard/lang/fr.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugin_title": "Tableau de bord",
|
||||
"plugin_description": "Ajoute un tableau de bord accessible depuis la navigation (n'a aucune utilisé - plugin de démo développeur seulement)",
|
||||
"menu_title": "Tableau de bord"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user