Compare commits

...

978 Commits

Author SHA1 Message Date
efa77bf534 Update README.md 2025-01-11 21:33:22 +00:00
bdb4e617e6 Update system/install-player-rpi.sh 2025-01-11 20:33:58 +00:00
e159e20b7c Update version.txt
All checks were successful
Release build and push docker image / build-and-push-release (push) Successful in 13m2s
2024-10-14 19:29:23 +00:00
1bf1c56fd8 Update .github/workflows/build-release.yml 2024-10-14 16:39:15 +00:00
ffc2b28a09 Updated README.md link 2024-10-14 16:31:26 +00:00
12aab494ad Updated image location. 2024-10-14 16:28:54 +00:00
2b2a1ba8bd Updated git locations 2024-10-14 16:27:20 +00:00
4bd104a742 Updated image locations. 2024-10-14 16:25:54 +00:00
c70d9f35c1 Updated git location 2024-10-14 16:23:26 +00:00
95bd859236 Update .github/workflows/build-nightly.yml 2024-10-14 15:58:26 +00:00
cb89522936 Update .github/workflows/build-pr.yml 2024-10-14 15:58:00 +00:00
6146308106 Update .github/actions/common-docker-build/action.yml 2024-10-14 15:56:59 +00:00
6fd4c48cc7 Update version.txt
All checks were successful
Release build and push docker image / build-and-push-release (push) Successful in 13m10s
2024-10-14 15:38:54 +00:00
589aa591ee Update .github/workflows/build-release.yml 2024-10-14 15:38:32 +00:00
990925e85e Update version.txt
Some checks failed
Release build and push docker image / build-and-push-release (push) Failing after 1m44s
2024-10-14 15:34:50 +00:00
ab8221889a Update docker-compose.yml 2024-10-14 15:04:19 +00:00
9e5d65651c Update system/install-player-rpi.sh 2024-10-14 15:03:18 +00:00
edbc159fad Updated git location 2024-10-14 15:02:41 +00:00
JRK
e278b9df6a
Merge pull request #148 from jr-k/develop
Release v2.4.5
2024-08-27 03:09:11 +02:00
jr-k
daef56f970 bump 2.5.0 2024-08-27 03:08:54 +02:00
JRK
e3ca756aa7
Merge pull request #145 from jr-k/feature/composition-content-type
Composition content type
2024-08-27 03:08:44 +02:00
jr-k
6208eb0840 custom aspect ratio for composition 2024-08-27 03:07:47 +02:00
jr-k
0830af4f56 custom aspect ratio for composition 2024-08-27 03:06:55 +02:00
jr-k
236b9324f5 aspect ratio picker 2024-08-27 02:40:39 +02:00
jr-k
603354cc4b some fixes 2024-08-27 02:31:00 +02:00
jr-k
7effdfeccd fully working text 2024-08-27 02:02:00 +02:00
jr-k
bfcc60323e fully working text 2024-08-27 01:59:41 +02:00
jr-k
7b1d361d69 text wip 2024-08-27 01:31:16 +02:00
jr-k
a121a9b7c3 fix grid composition 2024-08-27 01:28:03 +02:00
jr-k
6306a88e55 merge conflicts 2024-08-27 01:19:47 +02:00
jr-k
9d8b915905 add windows & macos doc 2024-08-27 01:19:22 +02:00
jr-k
0b51c9825d add pymediaonfo to deps 2024-08-27 01:10:51 +02:00
jr-k
b829fc026a fix video check 2024-08-27 01:08:27 +02:00
jr-k
0dcebf88af src/util/UtilVideo.py 2024-08-27 01:07:32 +02:00
jr-k
1ada29c5e9 setup script 2024-08-27 00:46:13 +02:00
jr-k
9d85aea268 Merge branch 'develop't 2024-08-27 00:41:25 +02:00
jr-k
98dc34358b add setup py 2024-08-27 00:41:16 +02:00
jr-k
753053e586 text wip 2024-08-26 15:53:53 +02:00
jr-k
c9a35ac933 wip 2024-08-26 15:34:21 +02:00
jr-k
6290b79886 wip 2024-08-26 15:33:44 +02:00
jr-k
1f7bd30ddf wip 2024-08-26 15:30:00 +02:00
jr-k
ccf29b1117 wip 2024-08-26 14:56:33 +02:00
jr-k
cd6f360628 wip 2024-08-26 14:53:58 +02:00
jr-k
a57ce2d840 wip 2024-08-26 14:25:44 +02:00
jr-k
aa36a17ef9 wip 2024-08-26 14:19:57 +02:00
jr-k
83be75967c text content type wip 2024-08-26 14:16:08 +02:00
JRK
e5f88d4002
Update README.md 2024-08-26 12:07:47 +02:00
JRK
ee3720c59e
Update README.md 2024-08-26 12:02:59 +02:00
jr-k
b1d11308d7 prepare for text type 2024-08-25 19:25:13 +02:00
jr-k
0c5c3e7dd5 tv news preset composition 2024-08-25 02:32:58 +02:00
jr-k
3851c4ef3e wip 2024-08-25 02:16:35 +02:00
jr-k
d27adbd77a wip 2024-08-25 02:11:27 +02:00
jr-k
1ef39dc0fb basic text content type 2024-08-25 01:49:53 +02:00
jr-k
f1a2e290ae wording 2024-08-25 01:36:41 +02:00
jr-k
3b1e78ae02 prepare for dynamic ratio 2024-08-25 00:35:48 +02:00
jr-k
21162495cb rename 2024-08-24 23:59:10 +02:00
jr-k
bc85e0c33f remove 2024-08-24 23:55:23 +02:00
jr-k
f6ade2ed53 better run script to force resolution 2024-08-24 22:43:23 +02:00
jr-k
3f3f3e4887 remove constraints x/y 2024-08-24 21:52:13 +02:00
jr-k
16d856c7df better preview mode 2024-08-24 21:48:18 +02:00
jr-k
d862268a32 fix shortcut 2024-08-24 21:20:22 +02:00
jr-k
db03ef0018 fix unwanted sql requests 2024-08-24 21:15:16 +02:00
jr-k
c00d967159 minor 2024-08-24 20:58:47 +02:00
jr-k
d38c72e4d6 lightweight dimensions detectors for pictures 2024-08-24 20:53:45 +02:00
jr-k
2680ee600b keyboard shortcuts 2024-08-24 20:48:46 +02:00
jr-k
6f3ed2cd80 add translation & content metadata 2024-08-24 20:42:04 +02:00
jr-k
e36752c4f9 remove element containment 2024-08-24 12:29:30 +02:00
jr-k
db321bf76e preview composition 2024-08-24 00:26:44 +02:00
jr-k
89e676424a light mode adjusts 2024-08-24 00:17:18 +02:00
jr-k
1658fe3bb9 light mode adjusts 2024-08-24 00:16:51 +02:00
jr-k
7ed5d9005c Merge branch 'develop' into feature/composition-content-type 2024-08-24 00:09:11 +02:00
jr-k
0c27de5fc1 Merge branch 'develop' 2024-08-24 00:08:59 +02:00
jr-k
20dd5e0ae2 doc 2024-08-24 00:08:55 +02:00
jr-k
442f02ff37 composition is ok 2024-08-23 22:56:44 +02:00
jr-k
56c421ed63 composition ok frontend 2024-08-23 20:42:24 +02:00
jr-k
18a199d609 at least one fallback, even if edit 2024-08-23 19:05:54 +02:00
jr-k
1fd0d99342 Merge branch 'develop' into feature/composition-content-type 2024-08-23 18:45:27 +02:00
JRK
2d18dbf23b
Merge pull request #144 from jr-k/develop
Release v2.4.4
2024-08-23 17:53:23 +02:00
jr-k
cf9dffb05c bump 2.4.4 2024-08-23 17:53:11 +02:00
jr-k
3a054fb055 fix youtube 2024-08-23 17:53:00 +02:00
jr-k
7c3bbc5323 Merge branch 'develop' 2024-08-20 19:56:05 +02:00
jr-k
9d88e2dbf4 omg 2024-08-20 19:56:01 +02:00
jr-k
aebb509965 Merge branch 'develop' 2024-08-20 19:36:22 +02:00
jr-k
616bab254a chromecast for no https and on non loopback server 2024-08-20 19:36:17 +02:00
jr-k
47ec669a38 Merge branch 'develop't p 2024-08-20 18:21:48 +02:00
jr-k
812a9ab0f4 better chromium install 2024-08-20 18:21:42 +02:00
JRK
e3235dc802
Merge pull request #142 from jr-k/develop
Release v2.4.3
2024-08-19 15:59:43 +02:00
jr-k
d25563f94e auto refresh if no playlist 2024-08-19 15:59:36 +02:00
jr-k
4eb3f0c8b1 bump 2.4.3
Some checks failed
PR build and push docker image / build-and-push-pr (push) Has been cancelled
2024-08-19 15:46:50 +02:00
JRK
1af0f098d1
Merge pull request #141 from jr-k/core/armbian-install
Armbian installation
2024-08-19 15:46:41 +02:00
jr-k
320b9242d5 fix installation scripts 2024-08-19 15:46:24 +02:00
JRK
258abb3bf5
Merge pull request #140 from jr-k/develop
Release v2.4.2
2024-08-19 11:13:51 +02:00
jr-k
84fdc64070 bump 2.4.2
Some checks are pending
PR build and push docker image / build-and-push-pr (push) Waiting to run
2024-08-19 11:13:38 +02:00
JRK
9266906d15
Merge pull request #139 from jr-k/bugfix/replace-all-old-browsers
[Fix] Uncaught TypeError: slide.innerHTML.replaceAll is not a function
2024-08-19 11:13:25 +02:00
jr-k
d760a537c9 use replace 2024-08-19 11:12:19 +02:00
JRK
dc51487d50
Merge pull request #137 from jr-k/master
Merge
2024-08-14 13:57:29 +02:00
JRK
63de70611d
Update obscreen-studio.service 2024-08-14 13:56:38 +02:00
jr-k
46fad2bef2 Merge branch 'developt ' 2024-08-13 15:45:57 +02:00
jr-k
e07dbf81d2 qfix 2024-08-13 15:45:53 +02:00
jr-k
22fb006d7a Merge branch 'devet plop' 2024-08-13 13:15:35 +02:00
jr-k
05f335b105 fix playlist slug 2024-08-13 13:15:27 +02:00
JRK
e801485ec1
Merge pull request #136 from jr-k/develop
Release v2.4.1
2024-08-13 12:40:10 +02:00
jr-k
28c28191a2 custom 404 ui + playlist enablement fix 2024-08-13 12:40:00 +02:00
jr-k
c280160e8b minor fix
Some checks are pending
PR build and push docker image / build-and-push-pr (push) Waiting to run
2024-08-13 12:16:11 +02:00
jr-k
03fbb34d95 wip 2024-08-13 12:15:58 +02:00
jr-k
4712047015 wip 2024-08-12 14:19:55 +02:00
jr-k
010d57bf4c float duration and minor fixes 2024-08-11 22:32:25 +02:00
jr-k
c81706061b float duration and minor fixes 2024-08-11 22:27:56 +02:00
jr-k
746b4234d2 fix external storage delegate duration for first slide 2024-08-10 16:04:31 +02:00
jr-k
97b7943fb1 permfix 2024-08-10 16:01:35 +02:00
jr-k
54335422cf flags multiline 2024-08-10 15:47:40 +02:00
jr-k
006ad75070 add owner to player 2024-08-10 15:44:25 +02:00
jr-k
1d9b723053 fix install script 2024-08-10 15:23:41 +02:00
jr-k
b14359b421 doc 2024-08-10 15:15:28 +02:00
jr-k
4b228cccf0 wip 2024-08-10 15:14:58 +02:00
jr-k
0bb72c03fc resolve conflicts 2024-08-09 02:21:57 +02:00
JRK
f9ebb9a053
Update README.md 2024-08-09 02:05:01 +02:00
JRK
95aa547196
Update README.md 2024-08-09 02:03:58 +02:00
jr-k
90acace1a7 Merge branch 'master' of https://github.com/jr-k/obscreen 2024-08-08 17:13:48 +02:00
jr-k
23079fedd9 fix 2024-08-08 17:13:32 +02:00
JRK
7028260458
Merge pull request #134 from jr-k/develop
Release v2.3.2
2024-08-08 13:54:51 +02:00
jr-k
d6de71b1e8 bump 2.4.0
Some checks are pending
PR build and push docker image / build-and-push-pr (push) Waiting to run
2024-08-08 13:54:34 +02:00
JRK
85b42274e4
Merge pull request #133 from jr-k/feature/better-content-cache-serve
Better content cache options
2024-08-08 13:54:19 +02:00
jr-k
608ee01f55 fix serving file for external_storage player 2024-08-08 12:30:16 +02:00
jr-k
e040cecf68 working flow 2024-08-08 12:12:25 +02:00
jr-k
377baf1edd pr trigger 2024-08-08 12:01:06 +02:00
jr-k
b6d5445b31 pr trigger 2024-08-08 11:59:20 +02:00
jr-k
68519d8407 pr trigger 2024-08-08 11:58:43 +02:00
jr-k
a37bb58ef3 pr trigger 2024-08-08 11:58:41 +02:00
jr-k
38ad5a85a4 pr trigger 2024-08-08 11:54:23 +02:00
jr-k
a735c0f4e8 pr trigger 2024-08-08 04:27:37 +02:00
jr-k
79a31b84c6 pr trigger 2024-08-08 04:26:57 +02:00
jr-k
5e04493329 pr trigger 2024-08-08 04:26:39 +02:00
jr-k
da1ffeb47d pr trigger 2024-08-08 04:25:43 +02:00
jr-k
ed8177e05f pr trigger 2024-08-08 04:24:51 +02:00
jr-k
2362673c6f pr trigger 2024-08-08 04:24:11 +02:00
jr-k
cdc25a2c1e pr trigger 2024-08-08 04:21:05 +02:00
jr-k
f1f444de39 pr trigger 2024-08-08 04:20:01 +02:00
jr-k
ea8f3b1334 pr trigger 2024-08-08 04:18:48 +02:00
jr-k
4453115030 pr trigger 2024-08-08 04:15:50 +02:00
jr-k
62ecdaac3b pr trigger 2024-08-08 04:15:09 +02:00
jr-k
842a722df8 pr trigger 2024-08-08 04:14:00 +02:00
jr-k
d7385c5d06 pr trigger 2024-08-08 03:53:01 +02:00
jr-k
fa934d8c9b pr trigger 2024-08-08 03:52:19 +02:00
jr-k
4810e10343 pr trigger 2024-08-08 03:51:23 +02:00
jr-k
4a9fb8e505 pr trigger 2024-08-08 03:49:45 +02:00
jr-k
1c0cce52b5 pr trigger 2024-08-08 03:48:10 +02:00
jr-k
77557d1699 pr trigger 2024-08-08 03:43:28 +02:00
jr-k
f582612619 pr trigger 2024-08-08 03:41:51 +02:00
jr-k
4bbc5ea972 pr trigger 2024-08-08 03:40:44 +02:00
jr-k
5b5f374255 pr trigger 2024-08-08 03:39:54 +02:00
jr-k
11df35209a pr trigger 2024-08-08 03:35:52 +02:00
jr-k
37105f6aa6 pr trigger 2024-08-08 03:30:47 +02:00
jr-k
e1413972bf pr trigger 2024-08-08 03:28:24 +02:00
jr-k
4dbf6e08fc pr trigger 2024-08-08 03:26:17 +02:00
jr-k
6113327075 pr trigger 2024-08-08 03:23:11 +02:00
jr-k
0ff94bc4f5 add pr build 2024-08-08 03:22:50 +02:00
jr-k
cf1fad0509 add pr build 2024-08-08 03:19:49 +02:00
jr-k
48e021a532 add pr build 2024-08-08 03:07:00 +02:00
jr-k
55ea9e5963 add pr build 2024-08-08 03:06:00 +02:00
jr-k
671e402db4 add nightly build 2024-08-08 02:51:47 +02:00
jr-k
c8064a9eb1 add nightly build 2024-08-08 02:49:44 +02:00
jr-k
69f8872755 add nightly build 2024-08-08 02:49:03 +02:00
jr-k
f6c0965f85 add nightly build 2024-08-08 02:44:03 +02:00
jr-k
bdb4f1d7bd done 2024-08-08 02:07:14 +02:00
jr-k
70efdf877e minor min 2024-08-08 00:23:44 +02:00
jr-k
69d477eeb1 minor min 2024-08-08 00:21:45 +02:00
jr-k
9ec77ede6f minor min 2024-08-08 00:09:53 +02:00
jr-k
e2e38c0db9 fix 2024-08-06 17:27:01 +02:00
jr-k
ea71566121 comments 2024-08-06 17:22:16 +02:00
jr-k
e919f76ea4 Merge branch 'develop' 2024-08-06 17:15:58 +02:00
jr-k
7a5234dbce fix 2024-08-06 17:15:55 +02:00
jr-k
fc9818ae4b Merge branch 'develop' 2024-08-06 13:18:07 +02:00
jr-k
da8cfa9222 cast js 2024-08-06 13:18:02 +02:00
jr-k
7ec157cf62 minor logs 2024-08-06 12:55:51 +02:00
JRK
9e54506dcc
Merge pull request #131 from jr-k/develop
Release v2.3.1
2024-08-06 12:23:15 +02:00
JRK
6b6fc91b6a
Update version.txt 2024-08-06 12:23:06 +02:00
JRK
0a422f748d
Merge pull request #130 from jr-k/feature/cast
Cast to chromecast preview
2024-08-06 12:22:20 +02:00
jr-k
4b09c6c26b handle cast link with python 2024-08-06 11:51:58 +02:00
jr-k
f8336b2220 remove blank line 2024-08-06 00:36:58 +02:00
jr-k
0dc3b6330b remove test 2024-08-06 00:36:42 +02:00
jr-k
6633e3f5b1 add cast feature 2024-08-06 00:34:44 +02:00
jr-k
3f1c5398a0 remove sudo in script 2024-08-05 23:44:26 +02:00
jr-k
f548f37a59 fix installation script 2024-08-05 23:40:40 +02:00
jr-k
3fbd4739ff update dockerfile 2024-08-05 01:10:25 +02:00
jr-k
ae4fddbfaa update dockerfile 2024-08-05 01:03:22 +02:00
jr-k
6333caa0b5 update dockerfile 2024-08-05 00:59:02 +02:00
jr-k
4fc7e1baec update dockerfile 2024-08-04 22:41:45 +02:00
jr-k
c1d95bf569 update dockerfile 2024-08-04 22:41:10 +02:00
jr-k
e9c4215f88 update dockerfile 2024-08-04 22:40:36 +02:00
jr-k
44d99e2be5 update dockerfile 2024-08-04 19:46:25 +02:00
jr-k
6580bd73ed update dockerfile 2024-08-04 19:45:15 +02:00
jr-k
377d99c0f1 update dockerfile 2024-08-04 19:43:14 +02:00
jr-k
a92188ae1f update dockerfile 2024-08-04 19:35:31 +02:00
jr-k
9b86106278 update dockerfile 2024-08-04 19:27:58 +02:00
jr-k
6892a0ded5 update dockerfile 2024-08-04 19:20:58 +02:00
jr-k
e3d954287d Merge branch 'develop' 2024-08-04 19:16:03 +02:00
jr-k
3715776f6d update dockerfile 2024-08-04 19:14:26 +02:00
JRK
62fe7ee1fe
Merge pull request #127 from jr-k/develop
Release v2.2.4
2024-08-04 18:04:17 +02:00
JRK
b1483bbf45
Merge pull request #126 from jr-k/feature/api
Api
2024-08-04 18:04:02 +02:00
jr-k
f717c6a82f bump 2024-08-04 18:02:53 +02:00
jr-k
d88db002f1 fully working api 2024-08-04 18:02:42 +02:00
jr-k
5fba1a4300 conditionnal token if plugin enabled 2024-08-04 17:42:06 +02:00
jr-k
1e5e9f0209 token management ok 2024-08-04 17:38:30 +02:00
jr-k
443650b217 full api with flask-restx/swagger 2024-08-04 17:18:14 +02:00
jr-k
fbb194e44c flask ok 2024-08-04 05:03:46 +02:00
jr-k
c7fc29ba99 wip swagger 2024-08-04 03:55:54 +02:00
jr-k
46a05cde84 working content api 2024-08-04 02:43:57 +02:00
jr-k
48e2f5bdfb wip 2024-08-04 02:13:13 +02:00
JRK
d869589b47
Merge pull request #125 from jr-k/develop
Release v2.2.3
2024-08-01 10:41:29 +02:00
jr-k
ff051beae7 bump 2024-08-01 10:41:19 +02:00
jr-k
aef16fe812 volume localtime for docker 2024-08-01 10:41:12 +02:00
jr-k
8a41d2d278 fix docker + add pause to preview iframe 2024-08-01 10:38:21 +02:00
jr-k
6e7ca0a1dc ignore fix 2024-08-01 10:22:28 +02:00
jr-k
e3c4726cda ignore fix 2024-08-01 10:21:14 +02:00
jr-k
c742f86924 ignore fix 2024-08-01 10:20:32 +02:00
jr-k
5b19668b38 add storage dir 2024-08-01 10:19:05 +02:00
JRK
80d6b6ef92
Update README.md 2024-08-01 10:13:12 +02:00
JRK
8cc56243e6
Merge pull request #124 from jr-k/develop
Release v2.2.2
2024-08-01 10:09:49 +02:00
jr-k
6c199dbe4a bump 2024-08-01 10:09:36 +02:00
jr-k
790b9d422f fix videos length on slide add only + fix player loop when 1 element only 2024-08-01 10:09:30 +02:00
jr-k
02c54566a9 fix and reintroduce animation exit effect 2024-08-01 09:50:54 +02:00
JRK
de8478ddea
Update FUNDING.yml 2024-08-01 09:19:18 +02:00
jr-k
b89ae3b7d1 Merge branch 'develop' 2024-07-30 18:51:59 +02:00
jr-k
cd6b8a72ca style 2024-07-30 18:51:39 +02:00
jr-k
d0661dd697 Merge brancht push 'develop' of https://github.com/jr-k/obscreen into develop 2024-07-30 18:51:29 +02:00
jr-k
d0ab0f6566 uuid to md5 file path for external storage is better 2024-07-30 18:51:21 +02:00
JRK
c5475478ad
Merge pull request #122 from jr-k/develop
Release v2.2.1
2024-07-30 18:35:01 +02:00
jr-k
ecd075c369 bump 2024-07-30 18:34:48 +02:00
jr-k
eae5611b07 bump 2024-07-30 18:34:11 +02:00
jr-k
564b064f50 fix video from external storage 2024-07-30 18:34:00 +02:00
jr-k
9e20bb65bd add dashboard 2024-07-30 18:20:12 +02:00
JRK
bb236ff1be
Merge pull request #121 from jr-k/develop
Release v2.2.0
2024-07-30 18:15:33 +02:00
jr-k
4d3c5fee3e ignore static plugin dir 2024-07-30 18:14:25 +02:00
jr-k
6d12f4124c remove artifacts 2024-07-30 18:13:55 +02:00
jr-k
143f6b5b44 bump 2.2.0 2024-07-30 18:13:00 +02:00
JRK
b305a60c8d
Update README.md 2024-07-27 09:43:31 +02:00
JRK
ed1939c7ac
Update README.md 2024-07-27 09:43:05 +02:00
JRK
5f4dbbd67b
Update README.md 2024-07-27 09:42:53 +02:00
jr-k
20dcb3659a fix preview_urls 2024-07-26 17:36:02 +02:00
jr-k
bd85d39af5 wip 2024-07-26 17:34:06 +02:00
jr-k
ea66a89ce1 fix video show view 2024-07-23 15:43:38 +02:00
jr-k
263eccc363 minor css 2024-07-23 14:03:11 +02:00
jr-k
49f6666c13 fix json item node player 2024-07-23 13:41:52 +02:00
jr-k
5d02a28468 resolve conflicts 2024-07-23 01:44:57 +02:00
jr-k
b4d682da69 restore 2024-07-23 01:42:09 +02:00
jr-k
53c377f274 done 2024-07-23 00:44:07 +02:00
jr-k
629ac4654d done 2024-07-23 00:44:07 +02:00
jr-k
53f6b46162 minor fix 2024-07-22 14:14:16 +02:00
jr-k
fc4273b585 minor fix 2024-07-22 14:14:16 +02:00
jr-k
4e45475ea9 doc 2024-07-22 03:59:35 +02:00
jr-k
e81508cc48 doc 2024-07-22 03:59:35 +02:00
jr-k
9692899c9b Merge branch 'develop' 2024-07-22 03:56:41 +02:00
jr-k
88e6fd0e63 Merge branch 'develop' 2024-07-22 03:56:41 +02:00
jr-k
eb84dc13dd better video duration probe 2024-07-22 03:56:27 +02:00
jr-k
f6e27ba9bf better video duration probe 2024-07-22 03:56:27 +02:00
JRK
edc95de507
Merge pull request #115 from jr-k/develop
Release v2.0.5
2024-07-22 03:23:58 +02:00
JRK
78f460f391 Merge pull request #115 from jr-k/develop
Release v2.0.5
2024-07-22 03:23:58 +02:00
jr-k
7810374fa4 2.1.0 bump 2024-07-22 03:23:40 +02:00
jr-k
4f775afa98 2.1.0 bump 2024-07-22 03:23:40 +02:00
JRK
e939b28ca5
Merge pull request #114 from jr-k/feature/external-storage
External Storage playback + Video duration as slide duration
2024-07-22 03:23:29 +02:00
JRK
5795622430 Merge pull request #114 from jr-k/feature/external-storage
External Storage playback + Video duration as slide duration
2024-07-22 03:23:29 +02:00
jr-k
78aacdee51 fix docker 2024-07-22 03:23:19 +02:00
jr-k
bd2d07c8eb fix docker 2024-07-22 03:23:19 +02:00
jr-k
3b04f4a47d word 2024-07-22 03:07:58 +02:00
jr-k
6d8c2df915 word 2024-07-22 03:07:58 +02:00
jr-k
52c64d13c3 remove log 2024-07-22 03:07:00 +02:00
jr-k
c2cf15d797 remove log 2024-07-22 03:07:00 +02:00
jr-k
605d3ce84f better delegate duration 2024-07-22 03:06:31 +02:00
jr-k
3ca566dfee better delegate duration 2024-07-22 03:06:31 +02:00
jr-k
4da9cf2e86 fix slide notification labels checkbox 2024-07-22 02:27:52 +02:00
jr-k
3c31ccf6fe fix slide notification labels checkbox 2024-07-22 02:27:52 +02:00
jr-k
9869d3c084 empty settings allowed for strings 2024-07-22 01:40:07 +02:00
jr-k
35cb49098b empty settings allowed for strings 2024-07-22 01:40:07 +02:00
jr-k
229e08fd36 external storage url variable 2024-07-22 01:37:31 +02:00
jr-k
47dfd430d2 external storage url variable 2024-07-22 01:37:31 +02:00
jr-k
eeb3a14ed3 external storage url variable 2024-07-22 01:36:12 +02:00
jr-k
f1e8f6ec69 external storage url variable 2024-07-22 01:36:12 +02:00
jr-k
0a39ef2fe6 update defaults 2024-07-22 01:26:42 +02:00
jr-k
26a407439b update defaults 2024-07-22 01:26:42 +02:00
jr-k
f7c20a0951 replacers 2024-07-22 01:24:25 +02:00
jr-k
3c94553ae2 replacers 2024-07-22 01:24:25 +02:00
jr-k
ae88a23a3c fix service script 2024-07-22 01:15:10 +02:00
jr-k
1073b96748 fix service script 2024-07-22 01:15:10 +02:00
jr-k
0e489074c7 fix service script 2024-07-22 01:12:48 +02:00
jr-k
d6f7e39075 fix service script 2024-07-22 01:12:48 +02:00
jr-k
32ee7378e9 check doc 2024-07-22 01:00:18 +02:00
jr-k
219a111120 check doc 2024-07-22 01:00:18 +02:00
jr-k
06c186b970 customize chroot dir for external storage 2024-07-22 00:25:43 +02:00
jr-k
04a74e2eec customize chroot dir for external storage 2024-07-22 00:25:43 +02:00
jr-k
f52b378611 working dyn video duration 2024-07-21 21:32:48 +02:00
jr-k
7a5b17f1f2 working dyn video duration 2024-07-21 21:32:48 +02:00
jr-k
69620d806d working dyn video duration 2024-07-21 21:32:11 +02:00
jr-k
fb8e4bf687 working dyn video duration 2024-07-21 21:32:11 +02:00
jr-k
e2a3eeaf43 remove logs 2024-07-21 20:07:21 +02:00
jr-k
b6a40e7e7c remove logs 2024-07-21 20:07:21 +02:00
jr-k
1e22c7f2dc fix 2024-07-21 20:06:39 +02:00
jr-k
e8c607b1ed fix 2024-07-21 20:06:39 +02:00
jr-k
b971c97673 fix 2024-07-21 20:05:29 +02:00
jr-k
9b391152af fix 2024-07-21 20:05:29 +02:00
jr-k
6f10b87e9f fix 2024-07-21 20:05:04 +02:00
jr-k
21c7dd841d fix 2024-07-21 20:05:04 +02:00
jr-k
240ebc3d88 fix 2024-07-21 19:58:45 +02:00
jr-k
7c7838967d fix 2024-07-21 19:58:45 +02:00
jr-k
079d0a0d2f fix 2024-07-21 19:56:46 +02:00
jr-k
e3ff140621 fix 2024-07-21 19:56:46 +02:00
jr-k
bd2f4b049d fix 2024-07-21 19:56:25 +02:00
jr-k
d593c2d646 fix 2024-07-21 19:56:25 +02:00
jr-k
3a236c3af3 fix var manager 2024-07-21 19:55:56 +02:00
jr-k
dafbfc3c9c fix var manager 2024-07-21 19:55:56 +02:00
jr-k
60324848e6 fix var manager 2024-07-21 19:55:01 +02:00
jr-k
197e393869 fix var manager 2024-07-21 19:55:01 +02:00
jr-k
4cbef95b5a fix var manager 2024-07-21 19:52:55 +02:00
jr-k
7435dbc4dc fix var manager 2024-07-21 19:52:55 +02:00
jr-k
17f5258d82 fix var manager 2024-07-21 19:51:50 +02:00
jr-k
fc5279aee9 fix var manager 2024-07-21 19:51:50 +02:00
jr-k
c082338d72 fix storage mounting 2024-07-21 19:42:27 +02:00
jr-k
c750b22bb6 fix storage mounting 2024-07-21 19:42:27 +02:00
jr-k
dcd8c2002e logs 2024-07-21 18:46:23 +02:00
jr-k
48dee68014 logs 2024-07-21 18:46:23 +02:00
jr-k
f8b6da0d4c change logic 2024-07-21 18:42:20 +02:00
jr-k
4383dc435a change logic 2024-07-21 18:42:20 +02:00
jr-k
46f06a6927 ok 2024-07-21 02:27:03 +02:00
jr-k
ca32c6e491 ok 2024-07-21 02:27:03 +02:00
jr-k
b7231a35a5 ok 2024-07-21 01:25:40 +02:00
jr-k
321dd1929f ok 2024-07-21 01:25:40 +02:00
jr-k
046666b524 doc 2024-07-19 11:57:15 +02:00
jr-k
03c39ea378 doc 2024-07-19 11:57:15 +02:00
jr-k
be3eea61f3 allow empty playlist for playgroup 2024-07-19 11:31:42 +02:00
jr-k
2c0a87ddb3 allow empty playlist for playgroup 2024-07-19 11:31:42 +02:00
jr-k
709c1eb8c4 fix calendar 2024-07-19 11:04:55 +02:00
jr-k
27e16052e7 fix calendar 2024-07-19 11:04:55 +02:00
jr-k
d358f934a7 user management for demo mode 2024-07-19 10:36:01 +02:00
jr-k
cf6372f45d user management for demo mode 2024-07-19 10:36:01 +02:00
jr-k
c792f7ebd1 fix player external url 2024-07-19 10:08:53 +02:00
jr-k
6bae530e53 fix player external url 2024-07-19 10:08:53 +02:00
jr-k
6417f4ab0b erge branch 'master' of https://github.com/jr-k/obscreen 2024-07-19 01:20:48 +02:00
jr-k
f7bf489601 erge branch 'master' of https://github.com/jr-k/obscreen 2024-07-19 01:20:48 +02:00
jr-k
4484791794 doc 2024-07-19 01:20:43 +02:00
jr-k
c96c2ff97b doc 2024-07-19 01:20:43 +02:00
JRK
2031f42229
Merge pull request #112 from jr-k/develop
Release v2.0.4
2024-07-19 01:16:59 +02:00
JRK
8ae599fade Merge pull request #112 from jr-k/develop
Release v2.0.4
2024-07-19 01:16:59 +02:00
jr-k
5c0fdf0029 bump 2.0.4 2024-07-19 01:16:45 +02:00
jr-k
a52744ab9b bump 2.0.4 2024-07-19 01:16:45 +02:00
jr-k
34ba9cd812 remove autoconfigure player file + add demo mode 2024-07-19 01:16:34 +02:00
jr-k
43a75c02f9 remove autoconfigure player file + add demo mode 2024-07-19 01:16:34 +02:00
jr-k
99d2518d92 new light mode screenshot 2024-07-19 00:18:04 +02:00
jr-k
d44fb2ce81 new light mode screenshot 2024-07-19 00:18:04 +02:00
jr-k
7c18b84ff4 Merge branch 'develop' 2024-07-19 00:17:33 +02:00
jr-k
77b55f6193 Merge branch 'develop' 2024-07-19 00:17:33 +02:00
jr-k
61cfc5e44f minor fix 2024-07-19 00:17:27 +02:00
jr-k
199111e48a minor fix 2024-07-19 00:17:27 +02:00
JRK
7db45caf20
Update README.md 2024-07-19 00:17:15 +02:00
JRK
d2d8ccd720 Update README.md 2024-07-19 00:17:15 +02:00
jr-k
91f33002e3 funding github 2024-07-18 23:31:44 +02:00
jr-k
027bc9d583 funding github 2024-07-18 23:31:44 +02:00
jr-k
e22d785b1e doc 2024-07-18 21:06:56 +02:00
jr-k
31b571db52 doc 2024-07-18 21:06:56 +02:00
jr-k
9838d6af39 doc 2024-07-18 21:06:12 +02:00
jr-k
a8df9a5f5a doc 2024-07-18 21:06:12 +02:00
jr-k
5e09bd3818 doc 2024-07-18 21:04:41 +02:00
jr-k
6be09e35f8 doc 2024-07-18 21:04:41 +02:00
jr-k
0c03f71412 test 2024-07-18 21:04:30 +02:00
jr-k
e13ac03e0c test 2024-07-18 21:04:30 +02:00
jr-k
5ff6eab1f7 test 2024-07-18 21:04:06 +02:00
jr-k
06466fa374 test 2024-07-18 21:04:06 +02:00
jr-k
711e323d0d test 2024-07-18 21:02:46 +02:00
jr-k
f024942b21 test 2024-07-18 21:02:46 +02:00
jr-k
b807b8f2d2 test 2024-07-18 21:02:27 +02:00
jr-k
f73ba58354 test 2024-07-18 21:02:27 +02:00
jr-k
44accc4d2b test 2024-07-18 21:01:23 +02:00
jr-k
32b9711ec9 test 2024-07-18 21:01:23 +02:00
jr-k
7f43db089c test 2024-07-18 21:00:50 +02:00
jr-k
b316108788 test 2024-07-18 21:00:50 +02:00
jr-k
a05b52ce74 test 2024-07-18 20:59:44 +02:00
jr-k
79fe4e3a9a test 2024-07-18 20:59:44 +02:00
jr-k
49418e56cf test 2024-07-18 20:57:16 +02:00
jr-k
9637865984 test 2024-07-18 20:57:16 +02:00
jr-k
f9cf7d9435 test 2024-07-18 20:54:52 +02:00
jr-k
9a62eb202b test 2024-07-18 20:54:52 +02:00
jr-k
cfae79d0f6 test 2024-07-18 20:47:56 +02:00
jr-k
415709ba91 test 2024-07-18 20:47:56 +02:00
jr-k
100065bd7e test 2024-07-18 20:44:40 +02:00
jr-k
ff0be0275f test 2024-07-18 20:44:40 +02:00
jr-k
623b5b47f8 test 2024-07-18 20:35:12 +02:00
jr-k
c1763d88c8 test 2024-07-18 20:35:12 +02:00
jr-k
50d300dd7a test 2024-07-18 20:34:19 +02:00
jr-k
a492bdde15 test 2024-07-18 20:34:19 +02:00
jr-k
88fce4c4a0 test 2024-07-18 20:25:05 +02:00
jr-k
47dedbc25d test 2024-07-18 20:25:05 +02:00
jr-k
dd3736ccd5 test 2024-07-18 20:24:15 +02:00
jr-k
74f767aaba test 2024-07-18 20:24:15 +02:00
jr-k
6c024ae354 test 2024-07-18 20:23:27 +02:00
jr-k
e4dce0d5ad test 2024-07-18 20:23:27 +02:00
jr-k
d9f549e481 test 2024-07-18 20:23:01 +02:00
jr-k
1c8598423d test 2024-07-18 20:23:01 +02:00
jr-k
b4e25e72f2 test 2024-07-18 20:21:37 +02:00
jr-k
348ecdd253 test 2024-07-18 20:21:37 +02:00
jr-k
3f9249b8bc test 2024-07-18 20:21:15 +02:00
jr-k
b85cb62588 test 2024-07-18 20:21:15 +02:00
jr-k
c3057cab0a test 2024-07-18 20:21:02 +02:00
jr-k
0c3e723c16 test 2024-07-18 20:21:02 +02:00
jr-k
2534a9b931 test 2024-07-18 20:20:07 +02:00
jr-k
6fbb96e54e test 2024-07-18 20:20:07 +02:00
jr-k
a2a373c43f doc 2024-07-18 20:17:04 +02:00
jr-k
7e9235503f doc 2024-07-18 20:17:04 +02:00
jr-k
5337eb81af max width 2024-07-18 19:12:40 +02:00
jr-k
eb56953815 max width 2024-07-18 19:12:40 +02:00
jr-k
462185f7b1 better alerting 2024-07-18 19:10:50 +02:00
jr-k
0a6c69bd57 better alerting 2024-07-18 19:10:50 +02:00
jr-k
e9ca3566b8 fix 2024-07-18 18:14:54 +02:00
jr-k
7818ac870b fix 2024-07-18 18:14:54 +02:00
jr-k
d5a8537216 revert oops 2024-07-18 18:01:00 +02:00
jr-k
309a8d8365 revert oops 2024-07-18 18:01:00 +02:00
jr-k
e5d2dc998e translations 2024-07-18 17:28:54 +02:00
jr-k
a0a02922dd translations 2024-07-18 17:28:54 +02:00
jr-k
334f53d91a Merge tbranch 'develop' 2024-07-18 17:22:17 +02:00
jr-k
faa0b24a17 Merge tbranch 'develop' 2024-07-18 17:22:17 +02:00
jr-k
e20d60ee8e ok 2024-07-18 17:22:12 +02:00
jr-k
57a5e0a816 ok 2024-07-18 17:22:12 +02:00
JRK
9d35550d5f
Merge pull request #111 from jr-k/develop
Release v2.0.3
2024-07-18 17:21:11 +02:00
JRK
94c4ad1bb2 Merge pull request #111 from jr-k/develop
Release v2.0.3
2024-07-18 17:21:11 +02:00
jr-k
f9cec4fedc translations 2024-07-18 17:21:02 +02:00
jr-k
e585353c00 translations 2024-07-18 17:21:02 +02:00
jr-k
a709160a40 2.0.3 2024-07-18 17:18:30 +02:00
jr-k
a38e977e0d 2.0.3 2024-07-18 17:18:30 +02:00
jr-k
103f986fcd seperate notifications 2024-07-18 17:18:19 +02:00
jr-k
38a82f9d2d seperate notifications 2024-07-18 17:18:19 +02:00
jr-k
7af7268e7d Merge branch 'develop' 2024-07-18 13:10:24 +02:00
jr-k
ed1bdaa52b Merge branch 'develop' 2024-07-18 13:10:24 +02:00
jr-k
f28bc91133 doc 2024-07-18 13:10:17 +02:00
jr-k
bb3a1688bf doc 2024-07-18 13:10:17 +02:00
jr-k
e2d4389757 Merge branch 'master' of https://github.com/jr-k/obscreen 2024-07-18 13:02:12 +02:00
jr-k
48186e883e Merge branch 'master' of https://github.com/jr-k/obscreen 2024-07-18 13:02:12 +02:00
jr-k
d83e4e8732 light mode modal background dark 2024-07-18 13:02:04 +02:00
jr-k
24f8bf459e light mode modal background dark 2024-07-18 13:02:04 +02:00
JRK
82cc513b4a
Merge pull request #109 from jr-k/develop
Release v2.0.2
2024-07-18 01:35:59 +02:00
JRK
86aae8ee34 Merge pull request #109 from jr-k/develop
Release v2.0.2
2024-07-18 01:35:59 +02:00
jr-k
e4690f9f96 bump 2024-07-18 01:35:46 +02:00
jr-k
41efcc7233 dark/light mode 2024-07-18 01:35:38 +02:00
jr-k
81dab72efb optimizations 2024-07-17 23:27:29 +02:00
jr-k
d9a9477ee2 roll 2024-07-17 22:30:16 +02:00
jr-k
cc01e95539 Merge branch 'develop' 2024-07-17 22:26:21 +02:00
jr-k
042ca3abfb show controls for video in pv mode 2024-07-17 18:24:14 +02:00
JRK
1099445c30 Merge pull request #108 from jr-k/develop
Release v2.0.1
2024-07-17 18:09:50 +02:00
JRK
8c707c9c8b Merge pull request #107 from jr-k/feature/better-explorer
Thumbnails and multiple Upload for similar file type
2024-07-17 18:08:35 +02:00
jr-k
5bae39b4c0 bump 2.0.1 2024-07-17 18:08:24 +02:00
jr-k
da57930dac better explr 2024-07-17 18:07:02 +02:00
jr-k
e3c348de36 better explr 2024-07-17 18:07:00 +02:00
jr-k
491e5f894e better explr 2024-07-17 17:15:00 +02:00
jr-k
d20b1359da better explr 2024-07-17 17:11:23 +02:00
jr-k
6dc1173164 rectangle select 2024-07-17 16:46:27 +02:00
jr-k
caea1a3adb dragndrop + bulk upload + bulk delete + shit/cmd/ctrl multi file selection + player default view if no fallback playlist 2024-07-17 16:13:43 +02:00
JRK
bfe7c61a99 Merge pull request #106 from jr-k/develop
Release v2.0.1
2024-07-17 11:08:09 +02:00
jr-k
ec0e34c882 translation menu 2024-07-17 11:07:50 +02:00
jr-k
07f62abccb Mergt e branch 'develop' 2024-07-17 01:30:16 +02:00
jr-k
2f0f875c5a doc 2024-07-17 01:30:08 +02:00
JRK
48728e3762 Merge pull request #103 from jr-k/develop
Release v2.0.0
2024-07-17 01:29:34 +02:00
jr-k
5c4fffa0e7 solve conflicts 2024-07-17 01:29:24 +02:00
JRK
1b3f890441 Merge pull request #97 from jr-k/feature/v2-ui
Ui v2
2024-07-17 01:27:24 +02:00
jr-k
7df05960b1 get all ip addresses 2024-07-17 01:26:15 +02:00
jr-k
7a99fd6b0b get all ip addresses 2024-07-17 01:21:27 +02:00
jr-k
90b00e8f24 player args query 2024-07-17 00:33:31 +02:00
jr-k
6537ce19d7 doc 2024-07-17 00:20:19 +02:00
jr-k
d5761b9afd doc 2024-07-17 00:17:49 +02:00
jr-k
630b0fb6c2 screenshots 2024-07-17 00:17:03 +02:00
jr-k
d1dbece28c fix xplr 2024-07-17 00:12:40 +02:00
jr-k
b2c772c580 fix xplr 2024-07-17 00:05:45 +02:00
jr-k
066b3814d3 fix xplr 2024-07-16 23:48:22 +02:00
jr-k
20a3f101a3 fix xplr 2024-07-16 23:45:50 +02:00
jr-k
127eb6dce9 cron desc 2024-07-16 22:42:22 +02:00
jr-k
f8c8d40614 wip 2024-07-16 22:05:20 +02:00
jr-k
3be54a813a ok 2024-07-16 21:33:34 +02:00
jr-k
8741ec12a9 logo check 2024-07-16 21:32:38 +02:00
jr-k
f62f976e58 playlist picking ok 2024-07-16 21:28:42 +02:00
jr-k
764d6e6a49 wip 2024-07-16 21:20:15 +02:00
jr-k
9cd71e5eda default playlist check startup 2024-07-16 21:13:05 +02:00
jr-k
650e72e94c default playlist ok 2024-07-16 21:10:38 +02:00
jr-k
6413ab668a user ok 2024-07-16 20:43:08 +02:00
jr-k
5e26facaef login page ok 2024-07-16 19:09:15 +02:00
jr-k
9387f21b74 node player groups wip 2024-07-16 18:43:35 +02:00
jr-k
3132a29197 node player groups wip 2024-07-16 18:35:35 +02:00
jr-k
e5803d2b04 node player groups wip 2024-07-16 18:34:34 +02:00
jr-k
6923a837fa node player groups wip 2024-07-16 18:08:11 +02:00
jr-k
e64c899cfd node player groups wip 2024-07-16 18:05:21 +02:00
jr-k
86548dd4aa node player groups wip 2024-07-16 17:07:05 +02:00
jr-k
a0f044d52e node player groups wip 2024-07-16 17:06:37 +02:00
jr-k
fb8c28b508 node player groups wip 2024-07-16 14:50:05 +02:00
jr-k
4e8477d098 fast player edit 2024-07-16 14:46:38 +02:00
jr-k
5082ad7b99 node player groups wip 2024-07-16 14:31:57 +02:00
jr-k
442854886e node player groups wip 2024-07-16 13:50:08 +02:00
jr-k
e1a5513c58 wip 2024-07-16 12:00:02 +02:00
jr-k
ea577c46ca slides ok 2024-07-16 03:53:04 +02:00
jr-k
22598a79eb slides ok 2024-07-16 03:48:50 +02:00
jr-k
27c229b676 slides ok 2024-07-16 03:30:09 +02:00
jr-k
21b15f283d wip 2024-07-16 03:21:09 +02:00
jr-k
9573db6aa1 slide wip 2024-07-16 02:17:31 +02:00
jr-k
e2fdc9adaa slide wip 2024-07-16 01:57:45 +02:00
jr-k
47e09cf412 slide wip 2024-07-16 01:54:31 +02:00
jr-k
1e305f01b7 slide wip 2024-07-16 01:45:33 +02:00
jr-k
e6a8d3cb34 wip 2024-07-15 23:43:18 +02:00
jr-k
f3dc3805c9 sidebar click 2024-07-13 15:45:21 +02:00
jr-k
29e81d44bd fixed enabled node player 2024-07-13 15:44:02 +02:00
jr-k
c652e63b6d wip 2024-07-13 15:34:35 +02:00
jr-k
148a01079b better explorer 2024-07-13 00:28:48 +02:00
jr-k
2855ba7133 logs ok 2024-07-12 23:25:29 +02:00
jr-k
e58fc32fc6 settings list & plugin ok 2024-07-12 16:55:52 +02:00
jr-k
415cbf29b7 prepare configuration pages 2024-07-12 15:47:10 +02:00
jr-k
27b1fa62ca wip 2024-07-12 00:40:48 +02:00
jr-k
deba8f3ba2 dyn type 2024-07-12 00:38:04 +02:00
jr-k
205be6a330 dyn type 2024-07-12 00:37:21 +02:00
JRK
aea19e5a83 Update README.md 2024-07-11 21:43:15 +02:00
jr-k
3aaa623804 cleaning 2024-07-11 21:16:55 +02:00
jr-k
0b31a69064 os colors 2024-07-11 21:11:46 +02:00
jr-k
99fb8e010d separate explorer in js file 2024-07-11 20:59:00 +02:00
jr-k
da57c38ddd players explorer ok 2024-07-11 20:52:51 +02:00
jr-k
f357b60ceb playlist start 2024-07-11 01:59:33 +02:00
jr-k
bc856ddec6 ascending param 2024-07-11 01:42:42 +02:00
jr-k
173319aa8c minor js required form 2024-07-11 01:38:51 +02:00
jr-k
ffe583b52c minor css 2024-07-11 01:32:38 +02:00
jr-k
022b094bda fix content creation in right folder + disable autoplay in preview mode 2024-07-11 01:30:42 +02:00
jr-k
6043547972 minor css 2024-07-11 01:23:45 +02:00
jr-k
5245010167 minor css 2024-07-11 01:20:28 +02:00
jr-k
1ff23b8bab minor style 2024-07-11 01:13:17 +02:00
jr-k
b673c92461 wip 2024-07-11 01:00:58 +02:00
jr-k
8dca0494b6 add content wip 2024-07-11 00:48:36 +02:00
jr-k
5e363fd4c8 menu 2 level dynamic ok 2024-07-11 00:43:46 +02:00
jr-k
4f63213dfc better explorer 2024-07-11 00:04:01 +02:00
jr-k
c137d938de user dyn top context 2024-07-10 22:32:31 +02:00
jr-k
bd4f97335d preview player & content edit preview ok 2024-07-10 22:30:20 +02:00
jr-k
a142def001 Merge branch 'develop' into feature/v2-ui 2024-07-10 22:03:27 +02:00
jr-k
d9966a0fe7 file add 2024-07-10 22:03:13 +02:00
jr-k
2f8b4932ed add content wip 2024-07-10 22:02:56 +02:00
JRK
af1e3009a6 Merge pull request #99 from jr-k/develop
Release v1.22.0
2024-07-10 22:01:51 +02:00
JRK
c4ebf4d308 Update version.txt 2024-07-10 22:01:23 +02:00
JRK
918ceb4852 Merge pull request #98 from jr-k/develop
Merge pull request #96 from jr-k/master
2024-07-10 22:00:44 +02:00
JRK
27721c795e fix, no content in slide 500 2024-07-10 22:00:22 +02:00
jr-k
f42e0ea069 add content wip 2024-07-10 21:20:27 +02:00
jr-k
91c4ed14fe add content wip 2024-07-10 21:07:58 +02:00
jr-k
f59fd90293 add content wip 2024-07-10 20:20:26 +02:00
jr-k
64d7de6b7a Merge branch 'develop' into feature/v2-ui 2024-07-10 19:52:54 +02:00
jr-k
85e56f7586 add form content 2024-07-10 19:52:53 +02:00
jr-k
e4bb6b99bd explorer move & add ok 2024-07-10 16:04:37 +02:00
jr-k
107537d5dd explorer move & add ok 2024-07-10 15:51:25 +02:00
jr-k
0d0fd61bb6 explorer move & add ok 2024-07-10 15:21:07 +02:00
jr-k
001ce36336 explorer move & add ok 2024-07-10 14:21:34 +02:00
jr-k
3aff1b4c1f file explorer ok 2024-07-09 19:14:54 +02:00
jr-k
d9e7c47899 wip 2024-07-09 18:31:17 +02:00
jr-k
3ed0d5cc90 wip 2024-07-09 14:39:21 +02:00
JRK
5e587a7664 Update setup-run-headless.md 2024-07-06 20:11:15 +02:00
JRK
25ee93d5eb Update setup-run-on-rpi.md 2024-07-06 20:11:10 +02:00
JRK
564bec6796 Update setup-run-headless.md 2024-07-06 20:10:10 +02:00
JRK
4a1bf3e7a1 Update setup-run-on-rpi.md 2024-07-06 20:09:26 +02:00
JRK
fc3e1b1da0 Update setup-run-on-rpi.md 2024-07-06 19:52:55 +02:00
JRK
eff3b4e49d Update setup-run-headless.md 2024-07-06 19:52:51 +02:00
jr-k
ee017e064f wip 2024-07-06 00:02:47 +02:00
JRK
9eac2f92e5 Update README.md 2024-07-05 15:00:08 +02:00
jr-k
920c8f96e0 wip 2024-07-05 01:53:32 +02:00
jr-k
ba4fd4844e wip 2024-07-05 01:52:40 +02:00
jr-k
ceae98b4f9 wip 2024-07-05 01:49:03 +02:00
jr-k
b9519ff074 wip 2024-07-05 01:46:45 +02:00
jr-k
6c7a5c6e06 wip 2024-07-05 01:45:39 +02:00
jr-k
4a53178cba wip 2024-07-05 01:42:39 +02:00
jr-k
506160deda wip 2024-07-05 00:59:42 +02:00
JRK
66c06d91c3 Merge pull request #96 from jr-k/master
Minor
2024-07-04 01:26:22 +02:00
JRK
ca9d46d909 Update es.json 2024-07-04 01:25:52 +02:00
JRK
a6a27a81fd Update en.json 2024-07-04 01:25:49 +02:00
jr-k
8d7efcc06a Merge branch 'develop' 2024-07-03 19:41:51 +02:00
jr-k
45bc1709f5 minor fixes 2024-07-03 19:41:39 +02:00
jr-k
6737281be8 Mt perge branch 'develop' 2024-07-03 19:31:40 +02:00
jr-k
02569b83e6 minor fixes 2024-07-03 19:31:34 +02:00
JRK
e3592d1149 Merge pull request #95 from jr-k/develop
Release v1.21.2
2024-07-03 14:31:36 +02:00
jr-k
7eabf070a8 bump 2024-07-03 14:31:25 +02:00
jr-k
593b40aaee add uuid to content + slide add with content at same time 2024-07-03 14:31:15 +02:00
JRK
0a8384c628 Merge pull request #94 from jr-k/develop
Release v1.21.1
2024-07-03 10:05:13 +02:00
jr-k
5f8bfc558c 1.21.1 2024-07-03 10:05:00 +02:00
JRK
26339db85b Merge pull request #93 from jr-k/feature/es-language
Add Spanish language
2024-07-03 10:04:42 +02:00
jr-k
fd2ef30b4a add spanish 2024-07-03 10:03:41 +02:00
JRK
3982cd377a Merge pull request #92 from jr-k/develop
Release v1.20.1
2024-07-02 18:12:22 +02:00
JRK
2675d79c92 Merge pull request #91 from jr-k/feature/remove-fleet-studio-and-separate-content-from-slide-plus-rename
Remove Fleet Studio + Separate Content From Slide + Rename
2024-07-02 18:11:54 +02:00
jr-k
d1bc021255 bump 1.21.0 2024-07-02 18:11:38 +02:00
jr-k
c2dd492aa1 Merge branch 'develop' into feature/remove-fleet-studio-and-separate-content-from-slide-plus-rename 2024-07-02 18:09:00 +02:00
jr-k
588e3c145a Mge branch 'develop' 2024-07-02 12:08:39 +02:00
jr-k
acb1dc900a fix youtube flush when notification slide 2024-07-02 12:08:28 +02:00
jr-k
d28dd31460 add translation for plugins + add italian to plugin 2024-07-02 11:34:03 +02:00
jr-k
65e7944d4d autorestart when edit plugin var 2024-07-02 10:47:39 +02:00
jr-k
15b3ea4a23 fix dyn auth enable 2024-07-02 10:39:10 +02:00
jr-k
08139a3845 merge 2024-07-02 10:21:09 +02:00
JRK
f768e38d18 Merge pull request #90 from jr-k/develop
Release v1.20.0
2024-06-30 14:39:29 +02:00
JRK
a165447c3c Merge pull request #87 from jr-k/feature/notification-date-start-end
Notification slides vs Auto(enable/disable) slides with date
2024-06-30 14:39:01 +02:00
jr-k
602627e17a bump 2024-06-30 14:38:52 +02:00
jr-k
83580033d3 remove console log 2024-06-30 14:24:41 +02:00
jr-k
3115897c92 fully working 2024-06-30 14:21:40 +02:00
jr-k
2d955137d7 Merge branch 'develop' into feature/notification-date-start-end 2024-06-29 11:20:31 +02:00
JRK
ff730e2271 Merge pull request #89 from jr-k/develop
Release v1.19.5
2024-06-28 22:14:14 +02:00
jr-k
ff03cc6c28 bump 2024-06-28 22:14:00 +02:00
jr-k
ec265ac11c fix player group removal 2024-06-28 22:13:54 +02:00
jr-k
c939c556ea doc rename 2024-06-28 18:51:57 +02:00
jr-k
13825c7a5f doc rename 2024-06-28 18:46:30 +02:00
jr-k
49169806ff doc rename 2024-06-28 18:45:27 +02:00
jr-k
17e1f99b65 t pMerge branch 'develop' 2024-06-28 18:42:48 +02:00
jr-k
f0e1d22791 doc rename 2024-06-28 18:42:46 +02:00
jr-k
8c2339cdde Merge branch 'master' of https://github.com/jr-k/obscreen 2024-06-28 17:19:59 +02:00
jr-k
824d1e78d9 Merge branch 'develop' 2024-06-28 17:19:54 +02:00
jr-k
1f4c9d2ddb add funding sponsor 2024-06-28 17:19:46 +02:00
JRK
4b9a21a5a6 Merge pull request #88 from jr-k/develop
Release v1.19.4
2024-06-26 23:28:24 +02:00
jr-k
82c6b75194 embed jquery and fontawesome 2024-06-26 23:28:09 +02:00
jr-k
80691061fc add notification to slide 2024-06-26 23:22:17 +02:00
jr-k
55164953df Merge branch 'master' of https://github.com/jr-k/obscreen 2024-06-21 15:09:27 +02:00
jr-k
52e1ddd014 handle reverse proxys 2024-06-21 15:09:20 +02:00
JRK
7648da2334 Merge pull request #84 from jr-k/develop
Release v1.19.3
2024-06-21 14:25:10 +02:00
jr-k
22bce37a9a bump 1.19.3 2024-06-21 14:24:53 +02:00
jr-k
e9dc9f0e0d troubleshoot section in doc 2024-06-21 14:23:32 +02:00
jr-k
9c9d2d72f0 fix video no load when intro slide was on 2024-06-21 14:18:51 +02:00
jr-k
eb70262ad6 regression fix css flatpickr 2024-06-21 14:13:23 +02:00
jr-k
f1dd369fa7 regression fix css flatpickr 2024-06-21 14:12:33 +02:00
jr-k
32c084ea0a issue templates 2024-06-21 14:09:13 +02:00
jr-k
af8ec817bc issue templates 2024-06-21 14:08:49 +02:00
jr-k
db264d9f14 issue templates 2024-06-21 14:08:02 +02:00
jr-k
9498ae5f00 issue templates 2024-06-21 14:07:06 +02:00
jr-k
8b86540b80 css to sass with vitejs 2024-06-21 13:56:19 +02:00
jr-k
bd54366d5c bump 1.19.2 2024-06-21 11:57:19 +02:00
jr-k
9c59c3ebfa add italian language 2024-06-21 11:57:03 +02:00
jr-k
bc05aeaa80 Merge branch 'master' of https://github.com/jr-k/obscreen 2024-06-16 22:07:02 +02:00
jr-k
a8bb356613 bump 1.19.1 2024-06-16 22:06:37 +02:00
JRK
d80f90d9d7 Merge pull request #81 from jr-k/develop
Release v1.20
2024-06-16 22:06:24 +02:00
jr-k
87a5620ccb ranch 'master' into develop 2024-06-16 22:06:02 +02:00
JRK
9f54982f37 Merge pull request #80 from ryanapp/fix/enable-user-plugins
Enable the use of user plugins
2024-06-16 22:05:44 +02:00
ryanapp
15aabd341f Enumerate user plugins as well as system plugins; resolve a couple of small typos 2024-06-16 16:56:34 +00:00
jr-k
ff1ab54376 minor fixes 2024-06-14 12:05:38 +02:00
JRK
e88e73cd68 Update setup-run-headless.md 2024-06-13 20:18:13 +02:00
jr-k
515d56509c revert 2024-06-13 19:58:16 +02:00
JRK
1b37d5f074 fix update 2024-06-13 18:41:55 +02:00
JRK
16f9e09e86 missing 2024-06-13 18:13:14 +02:00
JRK
bb432c9694 fix player routing based on host 2024-06-13 18:12:16 +02:00
JRK
bf0eb42fca fix network info 2024-06-13 18:09:59 +02:00
JRK
3b2889d20b Update ISSUE_TEMPLATE.md 2024-06-13 06:15:54 +02:00
JRK
54d01d03da Update ISSUE_TEMPLATE.md 2024-06-13 06:15:09 +02:00
JRK
b020ae84a7 Create ISSUE_TEMPLATE.md 2024-06-13 06:10:52 +02:00
jr-k
0a85fce882 Merge branch 'master' of https://github.com/jr-k/obscreen 2024-06-09 11:20:35 +02:00
jr-k
6d48ea904b fix null safe 2024-06-09 11:20:07 +02:00
JRK
510485a9b2 Merge pull request #77 from jr-k/develop
Release v1.19
2024-06-07 21:24:45 +02:00
jr-k
024ed51b1a bump 1.19 2024-06-07 21:23:51 +02:00
JRK
e8718ce104 Merge pull request #72 from jr-k/feature/fleet-players
Device management, groups, and shared slides across screens
2024-06-07 21:23:38 +02:00
jr-k
b164588b48 node player with ip ok in frontend 2024-06-07 21:23:19 +02:00
jr-k
697893e8c9 hide default node player group action buttons 2024-06-03 01:24:34 +02:00
jr-k
6e1b4d97ff backend ok, frontend ok, ready to use host and route playlists 2024-06-03 01:22:38 +02:00
jr-k
f8a9c5c360 icon 2024-06-03 00:34:29 +02:00
jr-k
5653a8f6a0 minor check utrack 2024-06-03 00:33:05 +02:00
jr-k
28aa40ee2b full backend entity management is ok 2024-06-03 00:30:21 +02:00
jr-k
70a9237951 generic user tracking 2024-06-03 00:01:01 +02:00
jr-k
becb98cfc5 add time markers on entities 2024-06-02 20:20:58 +02:00
jr-k
ed432c52fb fleet players back ok 2024-06-02 19:26:53 +02:00
jr-k
0a746632a3 fix front 2024-06-01 12:15:36 +02:00
jr-k
08d58048d2 Mert puge branch 'master' of https://github.com/jr-k/obscreen 2024-06-01 11:38:33 +02:00
jr-k
a304ba0fb2 typo 2024-06-01 11:38:27 +02:00
JRK
790914075d Merge pull request #71 from jr-k/develop
Release v1.18
2024-06-01 11:23:34 +02:00
jr-k
80bd703372 1.18 2024-06-01 11:23:24 +02:00
JRK
28c74f33c1 Merge pull request #70 from jr-k/feature/ip-refresh-and-intro-clock-seconds
Introduction slide (auto update ip address and show seconds in clock)
2024-06-01 11:23:16 +02:00
jr-k
f82a2b6062 done 2024-06-01 11:21:07 +02:00
jr-k
150382aa5a fix cron end 2024-05-27 19:48:38 +02:00
jr-k
fd8df06cb5 var to let/const in player + add welcome message with manager url in player + fix time sync playlists in player 2024-05-27 19:20:31 +02:00
jr-k
6dac88a0e3 default date for flatpickr fix 2024-05-27 16:46:16 +02:00
jr-k
9a0c7a8501 typo 2024-05-27 16:42:03 +02:00
JRK
6d2ca36511 Merge pull request #69 from jr-k/develop
Release v1.17
2024-05-27 16:15:05 +02:00
jr-k
3616d74c0a bump 1.17 2024-05-27 16:14:53 +02:00
jr-k
f7af72f169 dx logs 2024-05-27 16:14:09 +02:00
jr-k
9f7bdacf5a dx logs 2024-05-27 16:12:15 +02:00
jr-k
34e58cad37 add plugin description + use functional hook for button update plugin + add plugin store for controllers) 2024-05-27 16:11:25 +02:00
jr-k
5a2cf1c60c app name 2024-05-27 15:43:33 +02:00
jr-k
f3ca0d1f46 git updater plugin ok 2024-05-27 15:42:01 +02:00
jr-k
f99afcbbf1 git updater plugin ok 2024-05-27 15:41:39 +02:00
jr-k
bb2069f68d git updater plugin ok 2024-05-27 15:40:50 +02:00
jr-k
d1e5428b42 git updater plugin ok 2024-05-27 15:34:19 +02:00
jr-k
2c4130cc64 git updater plugin ok 2024-05-27 15:28:09 +02:00
jr-k
0ba7fa0593 git updater plugin ok 2024-05-27 15:24:31 +02:00
jr-k
d062fbc0a7 git updater plugin ok 2024-05-27 15:24:26 +02:00
jr-k
cec052d755 git updater plugin ok 2024-05-27 15:19:49 +02:00
jr-k
d862cae98e git updater plugin ok 2024-05-27 15:19:00 +02:00
jr-k
9d5d10712d remove no install recommends 2024-05-27 12:38:43 +02:00
jr-k
9a7f4315c2 Merge branch 'master' of https://github.com/jr-k/obscreen 2024-05-27 12:01:33 +02:00
jr-k
024f5f1d08 add doc img 2024-05-27 12:01:32 +02:00
JRK
3a6f221840 Update README.md 2024-05-27 12:01:07 +02:00
jr-k
acd15893d1 flatpickr fix + add end date in slide list 2024-05-27 10:51:54 +02:00
jr-k
e32ebec736 optim dockerfile 2024-05-27 10:26:13 +02:00
jr-k
7f5d2843c1 fix docker build for psutil python dep 2024-05-27 10:24:55 +02:00
jr-k
7b44dc4fbf clean css 2024-05-27 10:22:34 +02:00
jr-k
0f7aa54c6c Merge branch 'master' of https://github.com/jr-k/obscreen 2024-05-27 10:21:09 +02:00
jr-k
e22046882b revert design 2024-05-27 10:21:02 +02:00
jr-k
e261f4c4c9 add sysinfos 2024-05-27 01:50:53 +02:00
jr-k
74fceed06d add sysinfos 2024-05-27 01:48:46 +02:00
JRK
b2789c3195 Update setup-run-headless.md 2024-05-27 00:55:53 +02:00
JRK
fd5a9537d4 Update setup-run-headless.md 2024-05-27 00:55:30 +02:00
JRK
c32209c1f0 Update setup-run-headless.md 2024-05-27 00:54:43 +02:00
jr-k
ce7e44c551 silence print 2024-05-26 20:51:45 +02:00
jr-k
23718d229c video fix 2024-05-26 20:49:24 +02:00
jr-k
beb3ab5cd1 doc 2024-05-26 02:25:11 +02:00
jr-k
fd5d0212e9 add upgrade script 2024-05-26 02:17:25 +02:00
jr-k
5f21b127a8 add upgrade script 2024-05-26 02:16:36 +02:00
jr-k
df2e9b2c85 better sysinfo & console log handler 2024-05-26 02:13:31 +02:00
jr-k
0fbbf2e389 clean xscreensaver 2024-05-26 01:51:17 +02:00
jr-k
f47532c299 fixes for 1.16 2024-05-25 23:39:35 +02:00
jr-k
8cd4837993 editable default playlist time sync + more section for variables 2024-05-25 19:32:15 +02:00
jr-k
e9356c4424 add git keep 2024-05-25 19:11:51 +02:00
jr-k
8a9fe8eed5 add git keep 2024-05-25 19:10:37 +02:00
jr-k
13d25945a6 sync playlists 2024-05-25 19:04:16 +02:00
jr-k
3583b74262 Merge branch 'develop' 2024-05-25 14:55:23 +02:00
jr-k
e3fcdd5218 silence xscreensaver for now 2024-05-25 14:55:19 +02:00
JRK
832788b287 Merge pull request #67 from jr-k/develop
Release v1.16
2024-05-25 14:52:44 +02:00
jr-k
9f93d8a9c1 1.16 2024-05-25 14:52:29 +02:00
JRK
7978432a11 Merge pull request #64 from jr-k/feature/playlists
Playlists
2024-05-25 14:52:18 +02:00
jr-k
8c09ef5f62 cron schedule end ok for frontend 2024-05-25 14:50:34 +02:00
jr-k
094628f55c wip for schedule end backend ok 2024-05-25 14:48:16 +02:00
JRK
af17d924ae wip 2024-05-25 12:52:41 +02:00
JRK
a41a055579 wip 2024-05-25 12:41:59 +02:00
jr-k
62b4f6cc38 playlists 2024-05-25 09:31:40 +02:00
jr-k
1deb42e91f add xscreensaver 2024-05-24 14:25:58 +02:00
jr-k
f4b483581a Merge branch 'master' into feature/playlists 2024-05-24 14:25:07 +02:00
JRK
1a481d20c7 Update install-autorun-rpi.sh 2024-05-24 12:19:32 +02:00
jr-k
f5af0e34d9 rename manager > composer > studio 2024-05-23 10:39:15 +02:00
jr-k
389b6ad717 add playlist management 2024-05-21 20:35:46 +02:00
JRK
c934609fce Update README.md 2024-05-20 13:21:56 +02:00
jr-k
4420c2b1b3 add gitkeep for db 2024-05-19 00:15:49 +02:00
jr-k
a7c4cc2fd1 bad call fix 2024-05-18 22:58:34 +02:00
jr-k
fe6e96f8f2 fix type error 2024-05-18 20:55:10 +02:00
jr-k
904d485839 add apt package 2024-05-18 20:53:37 +02:00
JRK
1543ccd88a Merge pull request #63 from jr-k/develop
Release v1.15
2024-05-18 20:43:56 +02:00
jr-k
687750fd18 bump 1.15 2024-05-18 20:43:40 +02:00
JRK
6a26639453 Merge pull request #62 from jr-k/feature/sqlite-fault-tolerance
From PysonDB to SQLite for fault tolerance and power failure resilience
2024-05-18 20:43:27 +02:00
jr-k
3cacc9fb00 better restart management 2024-05-18 20:42:39 +02:00
jr-k
28cfae03a8 remove any reference to old json db file 2024-05-18 20:09:04 +02:00
jr-k
7ee148ea62 replace pysondb with sqlite 2024-05-18 20:07:18 +02:00
jr-k
76e4c18e80 erge branch 'develop' 2024-05-17 13:57:18 +02:00
jr-k
1203aae5b4 add model to slideshow dist 2024-05-17 13:57:09 +02:00
JRK
c127653d93 Merge pull request #58 from jr-k/develop
Release v1.15
2024-05-17 12:29:15 +02:00
jr-k
64a3d5083a fix script and systemd service for manager 2024-05-17 12:29:00 +02:00
JRK
2dad313353 Merge pull request #57 from jr-k/develop
Release v1.14
2024-05-17 12:11:37 +02:00
jr-k
67673ddc71 dyn user scripts 2024-05-16 16:12:40 +02:00
JRK
3b25e2e41b Update README.md 2024-05-16 13:30:36 +02:00
JRK
c44cd06bb5 doc layout 2024-05-16 11:46:11 +02:00
jr-k
b8a7caef10 fix youtube type check enum 2024-05-16 03:36:46 +02:00
jr-k
41f49860b5 restart player if manager restarts 2024-05-16 03:25:05 +02:00
jr-k
84a432dc9f iso 2024-05-16 03:11:30 +02:00
jr-k
dee0745745 fully working doc tested 2024-05-16 03:05:39 +02:00
jr-k
313f865de1 auto touch 2024-05-16 03:01:35 +02:00
jr-k
b9d0bcd3d3 rename files 2024-05-16 02:51:16 +02:00
jr-k
dd95313776 doc 2024-05-16 02:22:01 +02:00
jr-k
0e739d4b48 use of venv 2024-05-16 02:13:11 +02:00
JRK
0770452f78 Update install-autorun-rpi.sh 2024-05-16 02:00:06 +02:00
JRK
d56074df36 Update install-autorun-rpi.sh 2024-05-16 01:57:29 +02:00
JRK
ed14e94923 Update install-autorun-rpi.sh 2024-05-16 01:54:27 +02:00
jr-k
33661acb4b Merge branch 'develop' 2024-05-16 01:49:18 +02:00
jr-k
42045eaa0a fix doc link 2024-05-16 01:49:14 +02:00
JRK
3343b6ea9c Merge pull request #56 from jr-k/develop
Release v1.14
2024-05-16 01:43:45 +02:00
jr-k
c7d5369cb1 bump 1.14 2024-05-16 01:43:33 +02:00
jr-k
97bb40280d massive setup rework 2024-05-16 01:43:13 +02:00
JRK
04a37961ae Update README.md 2024-05-13 19:02:37 +02:00
JRK
a6e789009b Update README.md 2024-05-13 17:56:17 +02:00
JRK
29c92ce408 Merge pull request #53 from jr-k/develop
Release v1.14
2024-05-13 14:10:28 +02:00
jr-k
4f9d644138 change youtube id extract algorithm + variable as string if null fix + add youtube icon and fix icons on slideshow list + fix slide preview location 2024-05-13 14:10:06 +02:00
jr-k
d8fa0a6287 direct youtube id is now ok 2024-05-13 13:07:54 +02:00
JRK
45e96b7070 Merge pull request #52 from jr-k/develop
Release v1.13
2024-05-13 02:24:29 +02:00
jr-k
8c7753024d bump 1.13 2024-05-13 02:24:16 +02:00
JRK
fc12fdc610 Merge pull request #51 from jr-k/feature/auth-user-tracking
Authentication user tracking on slides
2024-05-13 02:23:12 +02:00
jr-k
1474977e37 do not allow disable last user + add anon user tag + save username if user is deleted 2024-05-13 02:17:29 +02:00
jr-k
3b853775b6 fully working user info about slide edition 2024-05-13 01:38:04 +02:00
jr-k
e85d6ecf3b add more info about user edits on slides 2024-05-13 01:37:19 +02:00
jr-k
a1424ef5ff add user tracking on slides + add favicon/manifest root urls 2024-05-13 01:14:41 +02:00
JRK
090173d00e Merge pull request #50 from jr-k/develop
Release v1.12
2024-05-12 23:20:36 +02:00
jr-k
1a146f10c6 handle login redirect when logged + add instructions on variable edition 2024-05-12 23:20:02 +02:00
jr-k
d1d89caccf Merge branch 'master' of https://github.com/jr-k/obscreen 2024-05-12 22:56:02 +02:00
jr-k
d01f2e4a88 Merge branch 'develop' 2024-05-12 22:55:58 +02:00
jr-k
89a64eda09 doc fix 2024-05-12 22:55:55 +02:00
JRK
a0ce7db4e8 Merge pull request #49 from jr-k/develop
Release v1.12
2024-05-12 22:54:16 +02:00
jr-k
02e279fdc3 no dummy file anymore, use /dev/null instead 2024-05-12 22:53:53 +02:00
JRK
efbbdf4f7e Merge pull request #48 from jr-k/develop
Release v1.12
2024-05-12 22:32:44 +02:00
jr-k
dc417fedfb fix youtube 2024-05-12 22:32:25 +02:00
JRK
2eee6bc620 Merge pull request #47 from jr-k/develop
Release v1.12
2024-05-12 22:26:01 +02:00
jr-k
abd897094d auth management fix 2024-05-12 22:25:44 +02:00
JRK
8091b6a338 Merge pull request #46 from jr-k/develop
Release v1.12
2024-05-12 21:16:45 +02:00
jr-k
73fcb2d5bf prepare schedule end + fix hot reload 2024-05-12 21:16:13 +02:00
JRK
60ff1d8870 Merge pull request #44 from jr-k/develop
Release v1.12
2024-05-12 19:59:55 +02:00
JRK
bc3188e84e Merge pull request #43 from jr-k/feautre/hot-reload-everywhere
Settings hot reload everywhere
2024-05-12 19:57:43 +02:00
jr-k
dfb4ad0c14 hot reload for language, fleet mode and size upload limit 2024-05-12 19:57:21 +02:00
JRK
6c4a2657ec Merge pull request #42 from jr-k/develop
Release v1.12
2024-05-12 19:41:19 +02:00
JRK
750d7a1206 Merge pull request #41 from jr-k/bugfix/auth-management
Fix auth management
2024-05-12 19:40:19 +02:00
jr-k
e74e3b090e add fix 2024-05-12 19:38:44 +02:00
JRK
9825e485a5 Merge pull request #40 from jr-k/develop
Release v1.12
2024-05-12 14:33:08 +02:00
jr-k
5a74dcaf87 bump 1.12 2024-05-12 14:32:49 +02:00
JRK
816e46ceb6 Merge pull request #39 from jr-k/develop
Release v1.12
2024-05-12 14:32:37 +02:00
jr-k
38376f5646 add path to readme 2024-05-12 14:31:53 +02:00
jr-k
46bd62f5fc Merge branch 'master' into develop 2024-05-12 14:31:18 +02:00
JRK
e642592db8 Update README.md 2024-05-12 14:30:43 +02:00
jr-k
fe4719c8d3 add warnings 2024-05-12 14:29:42 +02:00
jr-k
f25d71b580 add more informations about lx_file 2024-05-12 14:25:16 +02:00
jr-k
6df48b9263 Merge branch 'develop' 2024-05-10 11:02:56 +02:00
jr-k
64caa87235 fixes 2024-05-10 11:02:43 +02:00
JRK
fbf0b19455 Merge pull request #34 from jr-k/develop
Release v1.11
2024-05-09 22:17:39 +02:00
jr-k
87b70bc260 bump 1.11 2024-05-09 22:17:25 +02:00
JRK
9a110356db Merge pull request #33 from jr-k/feature/auth
Authentication for admin panel / back-end
2024-05-09 22:17:16 +02:00
jr-k
887eadb372 update doc 2024-05-09 22:17:09 +02:00
jr-k
024659d004 fix js translation + add style to login page 2024-05-09 22:13:09 +02:00
jr-k
3b0a099369 auth 99% ok missing frontend style 2024-05-09 21:59:42 +02:00
JRK
89445ddd14 Update README.md 2024-05-09 11:59:07 +02:00
JRK
f31010756c Merge pull request #32 from jr-k/develop
Release v1.10
2024-05-08 23:01:43 +02:00
JRK
ea673ccb5f bump 1.10 2024-05-08 23:01:26 +02:00
JRK
0656b5a12a Merge pull request #31 from jr-k/feature/youtube-type
Youtube slides
2024-05-08 23:01:11 +02:00
JRK
19c0a37ade working youtube 2024-05-08 22:59:54 +02:00
JRK
5989f52f96 missing youtube id extractor, player is ok 2024-05-08 22:40:27 +02:00
JRK
2a9d9809d9 Merge branch 'develop' 2024-05-08 19:07:41 +02:00
JRK
5c7c7419a4 add i18n to language variable 2024-05-08 19:07:04 +02:00
JRK
a1b20cc12f Merge pull request #30 from jr-k/develop
Release v1.9
2024-05-08 18:54:55 +02:00
JRK
4a5644a927 bump to 1.9 2024-05-08 18:54:40 +02:00
JRK
61a3160575 master merge :( 2024-05-08 18:54:25 +02:00
JRK
f80c8d1d66 Merge pull request #29 from jr-k/feature/auto-refresh-player-if-changes
Refreshable player and some player customizations through settings
2024-05-08 18:53:56 +02:00
JRK
cd26790ae5 add player refresh + add customizable durations for polling and default slide duration + ui edits + minor fixes 2024-05-08 18:46:39 +02:00
jr-k
94349b1dee Merge branch 'develop' 2024-05-07 14:16:42 +02:00
jr-k
dbbb22e43a Merge branch 'develop' of https://github.com/jr-k/obscreen into develop 2024-05-07 14:16:02 +02:00
jr-k
8425bdde1f fix for python3.9 2024-05-07 14:15:58 +02:00
jrk
7f11098a48 fix for python3.9 2024-05-07 14:15:05 +02:00
jr-k
2f8a7d1320 Merge branch 'develop' 2024-05-07 13:56:29 +02:00
jr-k
a40fab6b42 flush datetimepicker value if loop selected 2024-05-07 13:55:58 +02:00
jr-k
0aebfa4e60 erge branch 'develop' 2024-05-07 13:49:26 +02:00
jr-k
1ce9b925fe update port 2024-05-07 13:48:57 +02:00
JRK
cb51fffea8 Merge pull request #28 from jr-k/develop
Release v1.8
2024-05-07 13:48:11 +02:00
JRK
67d0d18b9d Merge pull request #27 from jr-k/core/update-port-for-player-url
Update port for player url
2024-05-07 13:47:07 +02:00
jr-k
b4e5aa1fe6 update port 2024-05-07 13:46:50 +02:00
JRK
85cf2de84f Merge pull request #26 from jr-k/develop
Release v1.8
2024-05-07 13:42:04 +02:00
jr-k
bdbab771f7 bump to 1.8 2024-05-07 13:39:48 +02:00
JRK
3d64e51638 Merge pull request #25 from jr-k/feature/remove-autoconfigure-nginx
Remove nginx autoconfiguration and better settings view
2024-05-07 13:39:39 +02:00
jr-k
8a42c06bad remove nginx autoconfigure + organize settings in UI with sections + better abstraction for db managers + better lang manager abstraction 2024-05-07 13:38:59 +02:00
jr-k
7b966b80c3 typo 2024-05-05 23:50:24 +02:00
jr-k
46a820b57a typo 2024-05-05 23:50:02 +02:00
jr-k
0805958e36 Merge branch 'develop' 2024-05-05 23:31:40 +02:00
jr-k
07c975ccd6 doc 2024-05-05 23:31:32 +02:00
JRK
459b550258 Merge pull request #23 from jr-k/develop
Better docker management
2024-05-05 23:14:31 +02:00
jr-k
f20edf1aa0 better docker use 2024-05-05 23:14:07 +02:00
JRK
8e70ccb770 Merge pull request #22 from jr-k/develop
Release v1.7
2024-05-05 20:53:49 +02:00
jr-k
7664907229 t pMerge branch 'develop' of https://github.com/jr-k/obscreen into develop 2024-05-05 20:53:31 +02:00
jr-k
d88d8e9f3b prepare release 2024-05-05 20:53:23 +02:00
JRK
0d695a4bf8 Merge pull request #21 from jr-k/feature/datetimepicker-slide-scheduler
Time and date picker for schedule feature
2024-05-05 20:52:54 +02:00
jr-k
f0ee618d87 Merge branch 'develop' into feature/datetimepicker-slide-scheduler 2024-05-05 20:51:27 +02:00
jr-k
a0c98e1faf add datetimepicker on schedule field 2024-05-05 20:50:00 +02:00
JRK
3785926257 Merge pull request #20 from jr-k/core/custom-max-upload-size
Allow custom max upload size
2024-05-05 19:36:24 +02:00
jr-k
69ca455bdb add database manager + add model updater + add unit to variables + allow custom limit for upload size 2024-05-05 19:35:18 +02:00
JRK
c962304565 Merge pull request #19 from jr-k/bugfix/null-safe-selectables-variable
Null safe check for Variable selectables field
2024-05-05 18:52:56 +02:00
jr-k
3b42543f08 check if selectables is None 2024-05-05 18:51:26 +02:00
JRK
72ce778e18 Merge pull request #18 from jr-k/core/wsgi-server
Do no use Flask as a server anymore, use Waitress as a WSGI server
2024-05-05 18:49:30 +02:00
jr-k
71ae6cf960 use waitress to serve app 2024-05-05 18:47:50 +02:00
JRK
a96ac37288 Merge pull request #12 from jr-k/develop
Release v1.6
2024-05-03 03:14:01 +02:00
jrk
35297a13d3 bump version 2024-05-03 03:13:43 +02:00
JRK
a82aa8144d Merge pull request #11 from jr-k/feature/schedulable-slides
[Feature Request] Schedule Slides
2024-05-03 03:13:13 +02:00
jrk
e431727937 fully working slide cron schedule frontend 2024-05-03 03:10:10 +02:00
jrk
6d2307efdf add cron slides to frontend, missing slide popup logic 2024-05-02 22:18:44 +02:00
jrk
89c1811d83 cron schedule fully working on backend 2024-05-02 21:57:57 +02:00
jrk
7ed31eb9ab Merge branch 'master' into feature/schedulable-slides 2024-05-02 19:24:52 +02:00
jrk
92cdae1baf Merge branch 'develop' into feature/schedulable-slides 2024-05-02 19:24:50 +02:00
jrk
eed49bb6d4 refacto player html + prepare crontab field on slide 2024-05-02 19:24:30 +02:00
JRK
3ebe5a36d0 Update README.md 2024-05-02 19:20:44 +02:00
JRK
80563bd542 Update README.md 2024-05-02 19:20:02 +02:00
JRK
d81ddbd9e9 Merge pull request #10 from jr-k/develop
Update README.md
2024-05-02 19:11:54 +02:00
JRK
1474e92040 Update README.md 2024-05-02 19:11:40 +02:00
JRK
9e711287fe Merge pull request #9 from jr-k/develop
add footer version
2024-05-02 16:23:10 +02:00
jrk
3b5dcfc59b add animations 2024-05-02 16:21:04 +02:00
jrk
58b3f46adc wording 2024-04-29 19:16:01 +02:00
jrk
f5f0ffbdc8 cliquable header, goto index 2024-04-29 19:06:30 +02:00
jrk
0f93c7bc60 wording 2024-04-29 19:04:11 +02:00
jrk
f6587f8029 ui variable placeholders 2024-04-29 19:03:15 +02:00
jrk
eca7ecefcc Merge branch 'develop' of https://github.com/jr-k/obscreen into develop 2024-04-29 18:58:49 +02:00
jrk
9ab8fe4ed0 add footer version 2024-04-29 18:57:33 +02:00
jrk
ffa1fdd515 add dummy file 2024-04-29 02:56:18 +02:00
jrk
b50de077ba add dummy file 2024-04-29 02:55:56 +02:00
JRK
5a1786c958 Merge pull request #8 from jr-k/develop
Release v1.4
2024-04-29 02:50:04 +02:00
jrk
cb53075bb9 bump version file 2024-04-29 02:49:37 +02:00
jrk
83c5178cc8 build only for push to master 2024-04-29 02:47:56 +02:00
JRK
ca987490b0 Merge pull request #7 from jr-k/develop
Release v1.2
2024-04-29 02:45:37 +02:00
jrk
149ec0f4d7 add v prefix to version docker builds 2024-04-29 02:45:30 +02:00
jrk
283065ee76 add latest to ci build 2024-04-29 02:42:31 +02:00
jrk
a710cf5055 add latest to ci build 2024-04-29 02:41:39 +02:00
JRK
f7a6b624f3 Merge pull request #6 from jr-k/develop
add docker build
2024-04-29 02:36:51 +02:00
jrk
c3f4ef4fc2 fix ci 2024-04-29 02:36:09 +02:00
jrk
b8b84c55ef add docker build 2024-04-29 02:35:08 +02:00
jrk
42afbbd805 better config management 2024-04-29 02:21:31 +02:00
jrk
e96cad5dad edit docker compose 2024-04-29 01:07:19 +02:00
jrk
4acb72aea5 update compose 2024-04-29 00:32:54 +02:00
JRK
dba1b78ee9 Update README.md 2024-04-28 17:46:11 +02:00
JRK
34f72f2772 Create LICENSE 2024-04-25 17:23:27 +02:00
JRK
848ffe53d5 Update README.md 2024-04-19 20:03:02 +02:00
JRK
5cda6ef8c7 Update README.md 2024-04-19 18:10:47 +02:00
JRK
b0458208db Update README.md 2024-04-09 16:43:13 +02:00
JRK
bad418cebf Update README.md 2024-04-09 16:40:07 +02:00
JRK
68cc338736 Update README.md 2024-03-22 15:08:04 +01:00
jr-k
9e960ed19b add docker 2024-03-22 14:59:23 +01:00
JRK
ad56f72500 Update README.md 2024-03-15 10:28:08 +01:00
JRK
485ffe9c78 Update README.md 2024-03-15 10:26:14 +01:00
JRK
b647d828f9 Update README.md 2024-03-15 10:25:23 +01:00
jr-k
0e851f22a0 missing css class on player 2024-03-13 13:25:27 +01:00
jr-k
9d83bf648d fleet mode tab persist 2024-03-04 17:32:52 +01:00
jr-k
dfedb6ef45 fix path 2024-03-04 17:26:04 +01:00
jr-k
9877f33771 lang 2024-03-01 13:09:19 +01:00
jr-k
255ab82c4d lang 2024-03-01 13:08:50 +01:00
jr-k
97b506cc12 remove artifacts 2024-03-01 13:07:49 +01:00
jr-k
7e2c11dd03 add functional hook 2024-03-01 13:06:27 +01:00
jr-k
dc52c816eb add functional hook 2024-03-01 13:06:20 +01:00
jr-k
2718b7bfc9 fix hooks and missing artifacts 2024-03-01 13:01:05 +01:00
jr-k
638093d78e custom controllers for plugin 2024-03-01 12:56:01 +01:00
jr-k
cb4e514425 add plugin vars separation 2024-03-01 11:03:08 +01:00
jr-k
2754d82c9e rename plugin 2024-03-01 04:30:40 +01:00
jr-k
c0c5c91c5c rename plugin 2024-03-01 04:30:03 +01:00
jr-k
65f086c70a add hooks 2024-03-01 04:29:45 +01:00
jr-k
cbda444af7 use id for lang plugins 2024-03-01 04:21:39 +01:00
jr-k
c4c4684068 add lang for plugins 2024-03-01 04:17:07 +01:00
jr-k
220cde48c9 doc about plugin 2024-03-01 04:01:33 +01:00
jr-k
d8fe5ec64e add plugin system 2024-03-01 04:01:15 +01:00
jr-k
54ba5b1eec minor 2024-03-01 01:41:23 +01:00
jr-k
f4f35c1fc9 minor 2024-03-01 01:40:42 +01:00
jr-k
f00f22d3b2 inject whole model manager to all controllers 2024-03-01 01:39:37 +01:00
jr-k
d31c8fe98b split main logic in files 2024-03-01 01:29:26 +01:00
jr-k
50846b6592 fix fleet url 2024-02-28 17:43:39 +01:00
jr-k
7648518c28 some fixes + type variables 2024-02-28 17:38:29 +01:00
jr-k
637b6197cb 100vh fleet mode for firefox 2024-02-28 15:20:42 +01:00
jr-k
abf2c52e35 add doctype 2024-02-28 15:16:13 +01:00
jr-k
e267605bab add failover 2024-02-28 15:07:57 +01:00
jr-k
465468bad4 enable loop for slideshow with 1 slide only 2024-02-28 15:05:25 +01:00
jr-k
0a9f817a30 missing return 2024-02-28 14:53:53 +01:00
JRK
b48d790c0d Update requirements.txt 2024-02-28 14:44:40 +01:00
JRK
1d71209325 Update README.md 2024-02-28 14:42:29 +01:00
jr-k
b38abe28a2 readme edit 2024-02-27 15:57:56 +01:00
jr-k
36eeb578ee add manage view 2024-02-27 15:54:27 +01:00
jr-k
e8a9590631 better ip guesser 2024-02-27 15:45:45 +01:00
jr-k
eae9beb46a translate month and day 2024-02-27 15:36:06 +01:00
jr-k
eb3cadf75a better restart logic 2024-02-27 15:27:37 +01:00
jr-k
b4e3e71a30 add restart logic 2024-02-27 15:16:34 +01:00
jr-k
e682c44c1a add config through UI 2024-02-27 15:03:40 +01:00
jr-k
638079f511 working fleet mode 2024-02-27 13:37:31 +01:00
jr-k
8bcc1646b8 fix ajax screen position + rename fleet list page 2024-02-27 12:59:40 +01:00
jr-k
20395c16be remove db 2024-02-27 12:54:16 +01:00
jr-k
54f54ad773 git ignore clean 2024-02-27 12:53:32 +01:00
jr-k
62995c7bc3 better fleet with host/port split 2024-02-27 12:52:23 +01:00
jr-k
eec10c81dc add fleet mode + split views in controllers 2024-02-27 12:45:03 +01:00
jr-k
e51061c984 some final cleaning 2024-02-27 11:07:31 +01:00
jr-k
792702b14f resize screenshot 2024-02-27 11:01:17 +01:00
JRK
34320aa35c Update README.md 2024-02-27 11:00:08 +01:00
jr-k
f7274737ae edit readme 2024-02-27 10:56:43 +01:00
jr-k
ab77bbdf55 split jinja views + add sysinfo page 2024-02-27 10:51:21 +01:00
jr-k
b75be8eb52 use pysondb2 instead + add lang strings 2024-02-27 10:18:59 +01:00
jr-k
33309dd70a more typings 2024-02-26 22:19:25 +01:00
jr-k
1a3953f242 clean js 2024-02-26 22:18:45 +01:00
jr-k
2cf67a6c1c fully working slide crud 2024-02-26 22:17:46 +01:00
jr-k
973fc653be crud for slide in gui 2024-02-26 14:33:02 +01:00
jr-k
4a939ace77 add lxfile to config 2024-02-25 16:59:04 +01:00
jr-k
0a8b4da8ca add lxfile to config 2024-02-25 16:58:44 +01:00
JRK
989ef64ca9 Update README.md 2024-02-19 16:17:06 +01:00
JRK
9b150f5864 Update README.md 2024-02-19 16:15:30 +01:00
JRK
170dad768a Update README.md 2024-02-19 15:45:44 +01:00
JRK
680cb07391 Update README.md 2024-02-19 15:45:01 +01:00
jr-k
010366234e add requirement pysondb 2024-02-19 15:39:24 +01:00
jr-k
be2a01b7ad missing rename 2024-02-19 15:38:13 +01:00
jr-k
6159f7e02f lang files 2024-02-19 15:37:44 +01:00
jr-k
c4409967f3 chmod 2024-02-19 15:37:24 +01:00
jr-k
2770f46745 slideshow template 2024-02-19 15:36:04 +01:00
jr-k
057b05a693 move files 2024-02-19 15:30:43 +01:00
jr-k
f7c4ab8861 add manage view 2024-02-19 15:19:21 +01:00
jr-k
44910c5345 add lang and config py 2024-02-19 15:19:04 +01:00
JRK
1970eb94af Rename reclame.service to obscreen.service 2024-02-18 23:04:28 +01:00
JRK
4e792729a6 Update slideshow.json.dist 2024-02-18 23:03:58 +01:00
JRK
733a3dc4e0 Rename reclame.jpg to sample.jpg 2024-02-18 23:03:48 +01:00
JRK
f9c2039ed7 Update reclame.service 2024-02-18 23:03:32 +01:00
JRK
82625ee5b5 Update and rename nginx-reclame to nginx-obscreen 2024-02-18 23:03:19 +01:00
JRK
c574d4c2fc Update player.jinja.html 2024-02-18 23:03:02 +01:00
JRK
a24ea48680 Rename reclame.py to obscreen.py 2024-02-18 23:02:48 +01:00
JRK
de1544624c Update README.md 2024-02-18 23:02:15 +01:00
jr-k
b5f9f2ca43 git ignore uploads but not logo file 2024-02-02 19:16:24 +01:00
jr-k
5cd46f5ae1 nginx for static 2024-02-02 16:29:10 +01:00
jr-k
107055504f add picture 2024-02-02 15:43:42 +01:00
jr-k
5f72c9f9d5 add picture 2024-02-02 15:38:07 +01:00
jr-k
55f2a0ef2f remove google translate popup 2024-02-02 14:56:45 +01:00
JRK
6f23b0d672 Update README.md 2024-02-02 14:31:28 +01:00
196 changed files with 7347 additions and 1508 deletions

View File

@ -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

View File

@ -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
View File

@ -1,2 +1 @@
liberapay: jierka
custom: https://paypal.me/jierka

View 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
View 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
View 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
View 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 }}

View File

@ -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
View File

@ -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/

View File

@ -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
View File

@ -0,0 +1,4 @@
include README.md
include LICENSE
docs/setup-run-on-rpi.md
docs/setup-run-headless.md

128
README.md
View File

@ -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="" />
[![Docker Pulls](https://badgen.net/docker/pulls/jierka/obscreen?icon=docker&label=docker%20pulls)](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
![Obscreen Screenshot](https://github.com/jr-k/obscreen/blob/master/docs/screenshot-playlist-edit.png "Obscreen Screenshot")
# 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

View File

@ -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
View 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()
});
});
});

View File

@ -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 () {

View File

@ -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');
});
});
});

View 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
View 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;
};
});
});

File diff suppressed because one or more lines are too long

1
data/www/js/lib/jscolor.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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();
});

View File

@ -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);
});

View File

@ -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',

View File

@ -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);
});

View 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
};
};
});

View 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));
});
});

View File

@ -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 () {

View File

@ -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');
});

View File

@ -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();

View File

@ -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
View File

View 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;

View File

@ -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;
}

View File

@ -1,6 +1,6 @@
menu {
width: 300px;
background: $gscale1;
background: $layoutBackground;
overflow-y: auto;
overflow-x: visible;
padding: 20px;

View File

@ -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;
}

View 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;}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -19,3 +19,17 @@
transform: rotate(2deg);
}
}
@keyframes blinkfade {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@ -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;

View File

@ -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;
}
}
}
}
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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';

View File

@ -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%);

View File

@ -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%);

View File

@ -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;
}
}

View 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);
}
}
}
}
}
}
}

View 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);
}
}
}
}
}
}
}

View File

@ -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 @@
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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;

View File

@ -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;
}
}
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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
View 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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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()

View 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 []

View 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'}

View 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)

View 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'})

View File

@ -0,0 +1,6 @@
from src.exceptions.HttpClientException import HttpClientException
class ContentNotFoundException(HttpClientException):
code = 404
description = "Content not found"

View File

@ -0,0 +1,6 @@
from src.exceptions.HttpClientException import HttpClientException
class ContentPathMissingException(HttpClientException):
code = 400
description = "Path is required"

View File

@ -0,0 +1,6 @@
from src.exceptions.HttpClientException import HttpClientException
class FolderNotEmptyException(HttpClientException):
code = 400
description = "Folder is not empty"

View File

@ -0,0 +1,6 @@
from src.exceptions.HttpClientException import HttpClientException
class FolderNotFoundException(HttpClientException):
code = 404
description = "Folder not found"

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View File

@ -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 []

View File

@ -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'])

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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 %}

View File

@ -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 %}

View 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')

View 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())
)

View 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"
}

View 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"
}

View 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