Compare commits

..

107 Commits

Author SHA1 Message Date
1a63232401 Update Everything 2022-09-24 03:07:39 +02:00
8183dcfcf4 Angular CLI update for packages - @angular/material@14 2022-09-24 03:05:13 +02:00
81b36e7f1a Update 2022-09-24 03:04:03 +02:00
3c3795e25a @angular/cli migration - update-angular-packages-version-prefix
Update Angular packages 'dependencies' and 'devDependencies' version prefix to '^' instead of '~'.
2022-09-24 02:56:05 +02:00
7a7a647be3 @angular/cli migration - remove-default-project-option
Remove 'defaultProject' option from workspace configuration. The project to use will be determined from the current working directory.
2022-09-24 02:56:01 +02:00
484b7cdfcd Angular CLI update for packages - @angular/core@14, @angular/cli@14 2022-09-24 02:56:01 +02:00
c8a7979132 @angular/cdk migration - migration-v13
Updates the Angular CDK to v13
2022-09-24 02:55:01 +02:00
45e5fd853e Angular CLI update for packages - @angular/material@13 2022-09-24 02:54:57 +02:00
5c66451524 Update 2022-09-24 02:54:17 +02:00
abc06d3181 @angular/cli migration - update-angular-config-v13
Remove deprecated options from 'angular.json' that are no longer present in v13.
2022-09-24 02:51:08 +02:00
2b4cabcb98 Angular CLI update for packages - @angular/core@13, @angular/cli@13 2022-09-24 02:51:07 +02:00
b3670aa0e0 Update @angular/material and @angular/cdk to 12.x 2022-09-24 02:47:31 +02:00
ba982001ba Update @angular/material and @angular/cdk to 11.x 2022-09-24 02:45:04 +02:00
d1ec708780 Update @angular/material and @angular/cdk to 10.x 2022-09-24 02:43:28 +02:00
f901708896 Remove runtime-config-loader 2022-09-24 02:26:37 +02:00
f0a7ee2d2b @angular/cli migration - update-zonejs
Update 'zone.js' to version 0.11.x. Read more about this here: https://github.com/angular/angular/blob/master/packages/zone.js/CHANGELOG.md#breaking-changes-since-zonejs-v0111
2022-09-24 02:24:19 +02:00
bc5818f50d @angular/cli migration - update-angular-config-v12
Remove deprecated options from 'angular.json' that are no longer present in v12.
2022-09-24 02:24:15 +02:00
a1264cb15b Angular CLI update for packages - @angular/core@12, @angular/cli@12 2022-09-24 02:24:15 +02:00
2e1eab3e2a Update 2022-09-24 02:22:34 +02:00
b69e771b75 Update 2022-09-24 02:22:25 +02:00
7d8ae1167e Update package-lock.json 2022-09-24 02:18:40 +02:00
b9e0302443 Delete package-lock.json 2022-09-24 02:14:48 +02:00
bb25b2801b Remove unused package 2022-09-23 23:20:09 +02:00
4d75c125ba Upgrade Angular to 10.x 2022-09-23 23:13:57 +02:00
5050e0cf60 Merge branch 'legacy' into new_frontend 2022-09-23 22:38:30 +02:00
84badb0e13 Retrieve old back end 2022-03-15 19:45:49 +01:00
10613d5c67 update package-lock.json 2022-03-07 18:33:56 +01:00
5bcf73a871 Merge pull request #32 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/jszip-3.7.1
Bump jszip from 3.2.2 to 3.7.1 in /frontend/ng-karaoqueue
2022-03-07 18:15:32 +01:00
645052027c Merge pull request #31 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/color-string-1.9.0
Bump color-string from 1.5.3 to 1.9.0 in /frontend/ng-karaoqueue
2022-03-07 18:15:20 +01:00
bf96424853 Merge pull request #30 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/karma-6.3.16
Bump karma from 4.4.1 to 6.3.16 in /frontend/ng-karaoqueue
2022-03-07 18:15:10 +01:00
d0e9e31a8c Merge pull request #29 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/url-parse-1.5.10
Bump url-parse from 1.4.7 to 1.5.10 in /frontend/ng-karaoqueue
2022-03-07 18:15:00 +01:00
415758b94d Merge pull request #27 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/follow-redirects-1.14.8
Bump follow-redirects from 1.9.0 to 1.14.8 in /frontend/ng-karaoqueue
2022-03-07 18:14:50 +01:00
89fca235ed Merge pull request #23 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/tar-4.4.19
Bump tar from 4.4.13 to 4.4.19 in /frontend/ng-karaoqueue
2022-03-07 18:14:28 +01:00
020a06d381 Merge pull request #20 from PhoenixTwoFive/dependabot/npm_and_yarn/backend/karaoqueue-backend/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7 in /backend/karaoqueue-backend
2022-03-07 18:14:06 +01:00
e09a9db75f Merge pull request #19 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7 in /frontend/ng-karaoqueue
2022-03-07 18:13:50 +01:00
654b8743d1 Merge pull request #17 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/dns-packet-1.3.4
Bump dns-packet from 1.3.1 to 1.3.4 in /frontend/ng-karaoqueue
2022-03-07 18:13:37 +01:00
3d8dfc9fe2 Bump jszip from 3.2.2 to 3.7.1 in /frontend/ng-karaoqueue
Bumps [jszip](https://github.com/Stuk/jszip) from 3.2.2 to 3.7.1.
- [Release notes](https://github.com/Stuk/jszip/releases)
- [Changelog](https://github.com/Stuk/jszip/blob/master/CHANGES.md)
- [Commits](https://github.com/Stuk/jszip/compare/v3.2.2...v3.7.1)

---
updated-dependencies:
- dependency-name: jszip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-07 17:13:18 +00:00
a642beee31 Merge pull request #16 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/hosted-git-info-2.8.9
Bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend/ng-karaoqueue
2022-03-07 18:13:13 +01:00
8d42db22b4 Bump color-string from 1.5.3 to 1.9.0 in /frontend/ng-karaoqueue
Bumps [color-string](https://github.com/Qix-/color-string) from 1.5.3 to 1.9.0.
- [Release notes](https://github.com/Qix-/color-string/releases)
- [Changelog](https://github.com/Qix-/color-string/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Qix-/color-string/commits/1.9.0)

---
updated-dependencies:
- dependency-name: color-string
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-07 17:13:10 +00:00
d7de33063e Merge pull request #15 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/lodash-4.17.21
Bump lodash from 4.17.19 to 4.17.21 in /frontend/ng-karaoqueue
2022-03-07 18:13:02 +01:00
607abc28f4 Merge pull request #13 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/ssri-6.0.2
Bump ssri from 6.0.1 to 6.0.2 in /frontend/ng-karaoqueue
2022-03-07 18:12:50 +01:00
4a1a2df725 Merge pull request #12 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/y18n-4.0.1
Bump y18n from 4.0.0 to 4.0.1 in /frontend/ng-karaoqueue
2022-03-07 18:12:35 +01:00
7cc614b496 Merge pull request #11 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/elliptic-6.5.4
Bump elliptic from 6.5.3 to 6.5.4 in /frontend/ng-karaoqueue
2022-03-07 18:12:14 +01:00
e4de261dfa Bump karma from 4.4.1 to 6.3.16 in /frontend/ng-karaoqueue
Bumps [karma](https://github.com/karma-runner/karma) from 4.4.1 to 6.3.16.
- [Release notes](https://github.com/karma-runner/karma/releases)
- [Changelog](https://github.com/karma-runner/karma/blob/master/CHANGELOG.md)
- [Commits](https://github.com/karma-runner/karma/compare/v4.4.1...v6.3.16)

---
updated-dependencies:
- dependency-name: karma
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 06:40:14 +00:00
7e66a42f9a Bump url-parse from 1.4.7 to 1.5.10 in /frontend/ng-karaoqueue
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.4.7 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.4.7...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-26 21:21:08 +00:00
f4c9b8d26a Bump follow-redirects from 1.9.0 to 1.14.8 in /frontend/ng-karaoqueue
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.9.0 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.9.0...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-13 02:28:48 +00:00
573b7a69da Bump tar from 4.4.13 to 4.4.19 in /frontend/ng-karaoqueue
Bumps [tar](https://github.com/npm/node-tar) from 4.4.13 to 4.4.19.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v4.4.13...v4.4.19)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-02 03:50:18 +00:00
da505e93a3 Bump path-parse from 1.0.6 to 1.0.7 in /backend/karaoqueue-backend
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-12 12:15:17 +00:00
ea7fba17a2 Bump path-parse from 1.0.6 to 1.0.7 in /frontend/ng-karaoqueue
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-11 19:21:03 +00:00
f3bf925e3d Bump dns-packet from 1.3.1 to 1.3.4 in /frontend/ng-karaoqueue
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-28 09:07:27 +00:00
d84eea9683 Bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend/ng-karaoqueue
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-11 09:15:19 +00:00
48b9bc843d Bump lodash from 4.17.19 to 4.17.21 in /frontend/ng-karaoqueue
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 21:15:40 +00:00
a9e16742e0 Bump ssri from 6.0.1 to 6.0.2 in /frontend/ng-karaoqueue
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-30 14:24:28 +00:00
02a66ba522 Bump y18n from 4.0.0 to 4.0.1 in /frontend/ng-karaoqueue
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-31 08:59:19 +00:00
f61df3e5ef Bump elliptic from 6.5.3 to 6.5.4 in /frontend/ng-karaoqueue
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-10 15:08:24 +00:00
80682ab883 Auth cleanup 2020-10-03 23:29:03 +02:00
ba02a01838 Update Example dotenv File 2020-10-03 15:43:31 +02:00
7cecf3a7a7 Fix Packages 2020-10-02 17:54:14 +02:00
f2b5dc93e2 Merge branch 'master' of github.com:PhoenixTwoFive/karaoqueue into master 2020-10-02 17:47:21 +02:00
b28dbba746 Add authentication and authorization 2020-10-02 17:47:13 +02:00
3990741131 Merge pull request #9 from PhoenixTwoFive/dependabot/npm_and_yarn/backend/karaoqueue-backend/bl-2.2.1
Bump bl from 2.2.0 to 2.2.1 in /backend/karaoqueue-backend
2020-09-25 17:18:38 +02:00
f552f8fefd Merge pull request #10 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/http-proxy-1.18.1
Bump http-proxy from 1.18.0 to 1.18.1 in /frontend/ng-karaoqueue
2020-09-25 17:17:04 +02:00
5e93f8592c Bump http-proxy from 1.18.0 to 1.18.1 in /frontend/ng-karaoqueue
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.18.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-10 23:16:24 +00:00
d0e90a5003 Bump bl from 2.2.0 to 2.2.1 in /backend/karaoqueue-backend
Bumps [bl](https://github.com/rvagg/bl) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/rvagg/bl/releases)
- [Commits](https://github.com/rvagg/bl/compare/v2.2.0...v2.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-03 21:18:01 +00:00
60dfea50c4 Merge pull request #4 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/websocket-extensions-0.1.4
Bump websocket-extensions from 0.1.3 to 0.1.4 in /frontend/ng-karaoqueue
2020-08-14 12:37:22 +02:00
1edc1b67bb Merge pull request #5 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/npm-registry-fetch-4.0.5
Bump npm-registry-fetch from 4.0.3 to 4.0.5 in /frontend/ng-karaoqueue
2020-08-14 12:37:11 +02:00
96f12b65c7 Merge pull request #6 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19 in /frontend/ng-karaoqueue
2020-08-14 12:36:59 +02:00
711745cd46 Merge pull request #7 from PhoenixTwoFive/dependabot/npm_and_yarn/frontend/ng-karaoqueue/elliptic-6.5.3
Bump elliptic from 6.5.2 to 6.5.3 in /frontend/ng-karaoqueue
2020-08-14 12:36:47 +02:00
4a52d5407f Merge pull request #8 from PhoenixTwoFive/dependabot/npm_and_yarn/backend/karaoqueue-backend/class-transformer-0.3.1
Bump class-transformer from 0.2.3 to 0.3.1 in /backend/karaoqueue-backend
2020-08-14 12:36:26 +02:00
a95ee7ccec Bump class-transformer in /backend/karaoqueue-backend
Bumps [class-transformer](https://github.com/typestack/class-transformer) from 0.2.3 to 0.3.1.
- [Release notes](https://github.com/typestack/class-transformer/releases)
- [Changelog](https://github.com/typestack/class-transformer/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/typestack/class-transformer/compare/v0.2.3...v0.3.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-07 16:25:31 +00:00
4a820e96fd Bump elliptic from 6.5.2 to 6.5.3 in /frontend/ng-karaoqueue
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-01 07:48:58 +00:00
e834a85375 Bump lodash from 4.17.15 to 4.17.19 in /frontend/ng-karaoqueue
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-19 01:43:53 +00:00
0a438d96f6 Bump npm-registry-fetch from 4.0.3 to 4.0.5 in /frontend/ng-karaoqueue
Bumps [npm-registry-fetch](https://github.com/npm/registry-fetch) from 4.0.3 to 4.0.5.
- [Release notes](https://github.com/npm/registry-fetch/releases)
- [Changelog](https://github.com/npm/npm-registry-fetch/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/registry-fetch/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-08 01:41:12 +00:00
e155078502 Move configuration to dotenv 2020-06-21 19:44:16 +02:00
097a5328e3 Bump websocket-extensions from 0.1.3 to 0.1.4 in /frontend/ng-karaoqueue
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-07 15:02:20 +00:00
c6c6b5da00 Add empty Controllers 2020-05-30 00:20:26 +02:00
c5e8ae0649 Minor API change
Put login and logout endpoints in their own node
2020-05-30 00:20:12 +02:00
91d02331f4 Add routing-controllers 2020-05-30 00:19:10 +02:00
7212f46191 Start new Node.js-based backend 2020-05-26 16:07:52 +02:00
27eb10d222 Remove Flask-based backend 2020-05-26 16:07:29 +02:00
a8ded945e2 Add Documentation 2020-05-26 16:05:56 +02:00
9ad74552fe Cosmetic change to buttons 2020-04-15 01:01:18 +02:00
578adaf893 Updates to search page 2020-04-15 00:51:26 +02:00
673fde9ee6 Readability fix
Adapt theme to make texts readable on dark backgrounds
2020-04-15 00:48:45 +02:00
e61c7a40cc VSCode configuration for frontend 2020-04-15 00:47:37 +02:00
013c52be61 VSCode Python configuration 2020-04-15 00:47:07 +02:00
ecd08d20ef Delete old Dockerfile 2020-04-15 00:46:33 +02:00
d3aea64880 Begin migrating backend to MongoDB 2020-04-15 00:45:00 +02:00
8687408d9c Integrate dockerized Mongodb configuration 2020-04-15 00:43:40 +02:00
ff3a1a3523 Add search 2020-03-26 02:45:55 +01:00
085f4dae96 Fix app background 2020-03-26 02:45:26 +01:00
27f15713cf Add mdi icons 2020-03-26 02:45:14 +01:00
c0c6fb8b06 Add constructors and accessors to models 2020-03-26 02:43:51 +01:00
869166f818 Send more data to the frontend 2020-03-26 02:40:44 +01:00
ca3d5f8a77 Add models 2020-03-25 18:06:26 +01:00
e989189055 Add services 2020-03-25 18:06:16 +01:00
281e8b5211 Add AppRouter and Components 2020-03-25 18:03:58 +01:00
e78e9da24c Add VS Code tasks 2020-03-25 18:02:09 +01:00
9317563047 Add npm scripts
Add npm script making ng serve accessible from other hosts.
2020-03-25 18:01:42 +01:00
c7fd25fae5 Split VS Code Workspace 2020-03-25 18:00:02 +01:00
8780e6c595 Style Changes 2020-03-25 17:59:38 +01:00
8652abe8dc Switch to SCSS 2020-03-11 02:31:38 +01:00
83a8c963a3 Update npm packages for frontend 2020-03-11 01:44:52 +01:00
e9f43c0169 Properly pass user input to SQLite and fix searches with umlauts.
Resolves #3.
2020-03-10 20:11:11 +01:00
a8871c1106 Fix song database update
Resolves #1
2019-12-13 15:29:58 +01:00
dc03dab0ce Update vscode launch definitins 2019-12-13 15:27:10 +01:00
160a64fc4a Update frontend dependencies 2019-12-13 14:47:27 +01:00
65 changed files with 14026 additions and 1821 deletions

View File

@ -1,40 +0,0 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"hostRequirements": {
"cpus": 4
},
"waitFor": "onCreateCommand",
"onCreateCommand": "curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash && sudo apt install -y libmariadb3 libmariadb-dev",
"updateContentCommand": "pip install -r backend/requirements.txt",
"portsAttributes": {
"5000": {
"label": "Application",
"onAutoForward": "openPreview"
}
},
"customizations": {
"codespaces": {
"openFiles": [
"templates/index.html"
]
},
"vscode": {
"extensions": [
"ms-python.python",
"batisteo.vscode-django",
"ms-python.flake8",
"ms-python.isort",
"ms-python.vscode-pylance",
"redhat.vscode-yaml",
"ms-azuretools.vscode-docker",
"donjayamanne.python-extension-pack"
]
}
},
"forwardPorts": [
5000
],
"features": {
"ghcr.io/devcontainers-contrib/features/angular-cli:2": {}
}
}

View File

@ -1,36 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
[*.md]
trim_trailing_whitespace = true
[*.py]
indent_size = 4
[*.js]
indent_size = 2
[*.html]
indent_size = 2
[*.css]
indent_size = 2
[*.scss]
indent_size = 2
[*.yaml]
indent_size = 2
[*.yml]
indent_size = 2

View File

@ -1,12 +0,0 @@
# MariaDB
MARIADB_ROOT_PASSWORD=mariadb_root_password
MARIADB_ROOT_HOST=localhost
MARIADB_DATABASE=karaoqueue
MARIADB_USER=karaoqueue
MARIADB_PASSWORD=mariadb_karaoqueue_password
# Karaoqueue
DEPLOYMENT_PLATFORM=Docker
DBSTRING="mysql+pymysql://karaoqueue:mariadb_karaoqueue_password@127.0.0.1:3306/karaoqueue?charset=utf8mb4"
INITIAL_USERNAME=admin
INITIAL_PASSWORD=changeme

View File

@ -1,3 +0,0 @@
[flake8]
ignore = E501
max-line-length = 120

View File

@ -1,23 +0,0 @@
name: Lint
on: [pull_request]
jobs:
flake8_py3:
runs-on: ubuntu-latest
steps:
- name: Setup Python
uses: actions/setup-python@v4.6.0
with:
python-version: '3.10'
architecture: x64
- name: Checkout PyTorch
uses: actions/checkout@master
- name: Install flake8
run: pip install flake8
- name: Run flake8
uses: suo/flake8-github-action@releases/v1
with:
checkName: 'flake8_py3' # NOTE: this needs to be the same as the job name
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

7
.gitignore vendored
View File

@ -87,7 +87,7 @@ ipython_config.py
# However, in case of collaboration, if having platform-specific dependencies or dependencies # However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not # having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies. # install all needed dependencies.
Pipfile.lock #Pipfile.lock
# celery beat schedule file # celery beat schedule file
celerybeat-schedule celerybeat-schedule
@ -137,7 +137,4 @@ data/
node_modules/ node_modules/
# Version identification file # Version identification file
.version .version
# Docker secrets
secrets.yml

14
.vscode/launch.json vendored
View File

@ -5,7 +5,7 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"preLaunchTask": "mariadb", "preLaunchTask": "versiondump",
"name": "Python: Flask", "name": "Python: Flask",
"type": "python", "type": "python",
"cwd": "${workspaceFolder}/backend", "cwd": "${workspaceFolder}/backend",
@ -14,9 +14,8 @@
"env": { "env": {
"FLASK_APP": "backend/app.py", "FLASK_APP": "backend/app.py",
"FLASK_ENV": "development", "FLASK_ENV": "development",
"FLASK_DEBUG": "1", "FLASK_DEBUG": "1"
}, },
"envFile": "${workspaceFolder}/.env.dev",
"args": [ "args": [
"run", "run",
"--no-debugger", "--no-debugger",
@ -25,7 +24,7 @@
"jinja": true "jinja": true
}, },
{ {
"preLaunchTask": "mariadb", "preLaunchTask": "versiondump",
"name": "Python: Flask (with reload)", "name": "Python: Flask (with reload)",
"type": "python", "type": "python",
"cwd": "${workspaceFolder}/backend", "cwd": "${workspaceFolder}/backend",
@ -36,7 +35,6 @@
"FLASK_ENV": "development", "FLASK_ENV": "development",
"FLASK_DEBUG": "1" "FLASK_DEBUG": "1"
}, },
"envFile": "${workspaceFolder}/.env.dev",
"args": [ "args": [
"run", "run",
"--no-debugger" "--no-debugger"
@ -44,7 +42,7 @@
"jinja": true "jinja": true
}, },
{ {
"preLaunchTask": "mariadb", "preLaunchTask": "versiondump",
"name": "Python: Flask (with reload, externally reachable)", "name": "Python: Flask (with reload, externally reachable)",
"type": "python", "type": "python",
"cwd": "${workspaceFolder}/backend", "cwd": "${workspaceFolder}/backend",
@ -55,7 +53,6 @@
"FLASK_ENV": "development", "FLASK_ENV": "development",
"FLASK_DEBUG": "1" "FLASK_DEBUG": "1"
}, },
"envFile": "${workspaceFolder}/.env.dev",
"args": [ "args": [
"run", "run",
"--no-debugger", "--no-debugger",
@ -64,7 +61,7 @@
"jinja": true "jinja": true
}, },
{ {
"preLaunchTask": "mariadb", "preLaunchTask": "versiondump",
"name": "Python: Flask (externally reachable)", "name": "Python: Flask (externally reachable)",
"type": "python", "type": "python",
"cwd": "${workspaceFolder}/backend", "cwd": "${workspaceFolder}/backend",
@ -75,7 +72,6 @@
"FLASK_ENV": "development", "FLASK_ENV": "development",
"FLASK_DEBUG": "1" "FLASK_DEBUG": "1"
}, },
"envFile": "${workspaceFolder}/.env.dev",
"args": [ "args": [
"run", "run",
"--no-debugger", "--no-debugger",

18
.vscode/settings.json vendored
View File

@ -1,17 +1,9 @@
{ {
"python.pythonPath": "/usr/bin/python", "python.pythonPath": "/usr/bin/python",
"python.testing.unittestArgs": [ "files.exclude": {
"-v", "**/.classpath": true,
"-s", "**/.project": true,
"./backend/tests", "**/.settings": true,
"-p", "**/.factorypath": true
"*test.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"emmet.includeLanguages": {
"django-html": "html"
} }
} }

14
.vscode/tasks.json vendored
View File

@ -1,5 +1,5 @@
{ {
// See https://go.microsoft.com/fwlink/?LinkId=733558 // See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format // for the documentation about the tasks.json format
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
@ -10,11 +10,13 @@
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "mariadb", "label": "Start Frontend",
"type": "shell", "type": "npm",
"command": "docker-compose -f docker-compose.yml up --remove-orphans", "script": "start",
"isBackground": true, "path": "frontend/ng-karaoqueue/",
"activeOnStart": false "problemMatcher": [
"$tsc"
]
} }
] ]
} }

View File

@ -1,23 +0,0 @@
FROM tiangolo/meinheld-gunicorn-flask:python3.9
# Currently unusable, mariadb is not available through installer for debian 12
# RUN apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 0xcbcb082a1bb943db
# RUN curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get dist-upgrade
# In the meantime, acquire the mariadb packages through apt
RUN apt-get install -y libmariadb3 libmariadb-dev
COPY ./backend/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
RUN pip install --no-cache-dir -U meinheld
ARG SOURCE_VERSION
ENV SOURCE_VERSION ${SOURCE_VERSION:-unknown}
COPY ./backend /app

View File

@ -0,0 +1,13 @@
{
"folders": [
{
"path": "./frontend/ng-karaoqueue"
},
{
"path": "backend"
},
{
"path": "docs"
}
]
}

View File

@ -1,48 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
autopep8 = "==2.0.2"
beautifulsoup4 = "==4.12.0"
bs4 = "==0.0.1"
certifi = "==2022.12.7"
charset-normalizer = "==3.1.0"
click = "==8.1.3"
flake8 = "==6.0.0"
flask = "==2.2.3"
flask-basicauth = "==0.2.0"
greenlet = "==2.0.2"
gunicorn = "==20.1.0"
idna = "==3.4"
itsdangerous = "==2.1.2"
jinja2 = "==3.1.2"
mariadb = "==1.1.6"
markupsafe = "==2.1.2"
mccabe = "==0.7.0"
mysql = "==0.0.3"
mysqlclient = "==2.1.1"
numpy = "==1.24.2"
packaging = "==23.0"
pandas = "==1.5.3"
pycodestyle = "==2.10.0"
pyflakes = "==3.0.1"
pymysql = "==1.0.3"
python-dateutil = "==2.8.2"
pytz = "==2023.3"
requests = "==2.28.2"
six = "==1.16.0"
soupsieve = "==2.4"
sqlalchemy = "==2.0.7"
toml = "==0.10.2"
tomli = "==2.0.1"
typing-extensions = "==4.5.0"
urllib3 = "==1.26.15"
werkzeug = "==2.2.3"
[dev-packages]
[requires]
python_version = "3.9"
python_full_version = "3.9.17"

View File

@ -1,5 +1,4 @@
from flask import Flask, render_template, abort, request, redirect, send_from_directory, jsonify from flask import Flask, render_template, Response, abort, request, redirect, send_from_directory
from flask.wrappers import Response
import helpers import helpers
import database import database
import data_adapters import data_adapters
@ -7,19 +6,17 @@ import os
import json import json
from flask_basicauth import BasicAuth from flask_basicauth import BasicAuth
from helpers import nocache from helpers import nocache
from werkzeug.utils import secure_filename
app = Flask(__name__, static_url_path='/static') app = Flask(__name__, static_url_path='/static')
basic_auth = BasicAuth(app) basic_auth = BasicAuth(app)
accept_entries = True accept_entries = False
@app.route("/") @app.route("/")
def home(): def home():
if basic_auth.authenticate(): if basic_auth.authenticate():
return render_template('main_admin.html', list=database.get_list(), auth=basic_auth.authenticate(), debug=app.config['DEBUG']) return render_template('main_admin.html', list=database.get_list(), auth=basic_auth.authenticate())
else: else:
return render_template('main.html', list=database.get_list(), auth=basic_auth.authenticate(), debug=app.config['DEBUG']) return render_template('main.html', list=database.get_list(), auth=basic_auth.authenticate())
@app.route("/favicon.ico") @app.route("/favicon.ico")
@ -38,13 +35,13 @@ def enqueue():
if not helpers.is_valid_uuid(client_id): if not helpers.is_valid_uuid(client_id):
print(request.data) print(request.data)
abort(400) abort(400)
name = request.json['name'].strip() name = request.json['name']
song_id = request.json['id'] song_id = request.json['id']
if request.authorization: if request.authorization:
entry_id = database.add_entry(name, song_id, client_id) database.add_entry(name, song_id, client_id)
return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json') return Response('{"status":"OK"}', mimetype='text/json')
else: else:
if helpers.get_accept_entries(app): if accept_entries:
if not request.json: if not request.json:
print(request.data) print(request.data)
abort(400) abort(400)
@ -54,10 +51,10 @@ def enqueue():
abort(400) abort(400)
name = request.json['name'] name = request.json['name']
song_id = request.json['id'] song_id = request.json['id']
if database.check_queue_length() < int(app.config['MAX_QUEUE']): if database.check_queue_length() < app.config['MAX_QUEUE']:
if database.check_entry_quota(client_id) < int(app.config['ENTRY_QUOTA']): if database.check_entry_quota(client_id) < app.config['ENTRY_QUOTA']:
entry_id = database.add_entry(name, song_id, client_id) database.add_entry(name, song_id, client_id)
return Response(f"""{{"status":"OK", "entry_id":{entry_id}}}""", mimetype='text/json') return Response('{"status":"OK"}', mimetype='text/json')
else: else:
return Response('{"status":"Du hast bereits ' + str(database.check_entry_quota(client_id)) + ' Songs eingetragen, dies ist das Maximum an Einträgen die du in der Warteliste haben kannst."}', mimetype='text/json', status=423) return Response('{"status":"Du hast bereits ' + str(database.check_entry_quota(client_id)) + ' Songs eingetragen, dies ist das Maximum an Einträgen die du in der Warteliste haben kannst."}', mimetype='text/json', status=423)
else: else:
@ -68,14 +65,14 @@ def enqueue():
@app.route("/list") @app.route("/list")
def songlist(): def songlist():
return render_template('songlist.html', list=database.get_song_list(), auth=basic_auth.authenticate(), debug=app.config['DEBUG']) return render_template('songlist.html', list=database.get_song_list(), auth=basic_auth.authenticate())
@app.route("/settings") @app.route("/settings")
@nocache @nocache
@basic_auth.required @basic_auth.required
def settings(): def settings():
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes(), debug=app.config['DEBUG']) return render_template('settings.html', app=app, auth=basic_auth.authenticate())
@app.route("/settings", methods=['POST']) @app.route("/settings", methods=['POST'])
@ -84,33 +81,16 @@ def settings():
def settings_post(): def settings_post():
entryquota = request.form.get("entryquota") entryquota = request.form.get("entryquota")
maxqueue = request.form.get("maxqueue") maxqueue = request.form.get("maxqueue")
theme = request.form.get("theme") if entryquota.isnumeric() and int(entryquota) > 0:
username = request.form.get("username") app.config['ENTRY_QUOTA'] = int(entryquota)
password = request.form.get("password")
changed_credentials = False
if entryquota.isnumeric() and int(entryquota) > 0: # type: ignore
app.config['ENTRY_QUOTA'] = int(entryquota) # type: ignore
else: else:
abort(400) abort(400)
if maxqueue.isnumeric and int(maxqueue) > 0: # type: ignore if maxqueue.isnumeric and int(maxqueue) > 0:
app.config['MAX_QUEUE'] = int(maxqueue) # type: ignore app.config['MAX_QUEUE'] = int(maxqueue)
else: else:
abort(400) abort(400)
if theme is not None and theme in helpers.get_themes():
helpers.set_theme(app, theme) return render_template('settings.html', app=app, auth=basic_auth.authenticate())
else:
abort(400)
if username != "" and username != app.config['BASIC_AUTH_USERNAME']:
app.config['BASIC_AUTH_USERNAME'] = username
changed_credentials = True
if password != "":
app.config['BASIC_AUTH_PASSWORD'] = password
changed_credentials = True
helpers.persist_config(app=app)
if changed_credentials:
return redirect("/")
else:
return render_template('settings.html', app=app, auth=basic_auth.authenticate(), themes=helpers.get_themes(), debug=app.config['DEBUG'])
@app.route("/api/queue") @app.route("/api/queue")
@ -124,7 +104,7 @@ def queue_json():
@nocache @nocache
@basic_auth.required @basic_auth.required
def played_list(): def played_list():
return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate(), debug=app.config['DEBUG']) return render_template('played_list.html', list=database.get_played_list(), auth=basic_auth.authenticate())
@app.route("/api/songs") @app.route("/api/songs")
@ -139,134 +119,29 @@ def songs():
@basic_auth.required @basic_auth.required
def update_songs(): def update_songs():
database.delete_all_entries() database.delete_all_entries()
helpers.reset_current_event_id(app)
status = database.import_songs( status = database.import_songs(
helpers.get_songs(helpers.get_catalog_url())) helpers.get_songs(helpers.get_catalog_url()))
print(status) print(status)
return Response('{"status": "%s" }' % status, mimetype='text/json') return Response('{"status": "%s" }' % status, mimetype='text/json')
@app.route("/api/songs/compl") # type: ignore @app.route("/api/songs/compl")
@nocache @nocache
def get_song_completions(input_string=""): def get_song_completions(input_string=""):
input_string = request.args.get('search', input_string) input_string = request.args.get('search', input_string)
if input_string != "": if input_string != "":
result = [list(x) for x in database.get_song_completions(input_string=input_string)] print(input_string)
return jsonify(result) list = database.get_song_completions(input_string=input_string)
return Response(json.dumps(list, ensure_ascii=False).encode('utf-8'), mimetype='text/json')
else: else:
return 400 return 400
@app.route("/api/songs/search") @app.route("/api/entries/delete/<entry_id>")
@nocache
def query_songs_with_details(input_string=""):
input_string = request.args.get("q", input_string)
if input_string == "":
return Response(status=400)
result = []
for x in database.get_songs_with_details(input_string):
# Turn row into dict. Add field labels.
result.append(dict(zip(['karafun_id', 'title', 'artist', 'year', 'duo', 'explicit', 'styles', 'languages'], x)))
return jsonify(result)
@app.route("/api/songs/suggest")
@nocache
def query_songs_with_details_suggest(input_string=""):
input_string = request.args.get("count", input_string)
if input_string == "":
return Response(status=400)
result = []
if not input_string.isnumeric():
return Response(status=400)
count: int = int(input_string)
for x in database.get_song_suggestions(count):
# Turn row into dict. Add field labels.
result.append(dict(zip(['karafun_id', 'title', 'artist', 'year', 'duo', 'explicit', 'styles', 'languages'], x)))
return jsonify(result)
@app.route("/api/songs/stats")
@nocache
# Return the data from long_term_stats as json
def get_stats():
db_result = database.get_long_term_stats()
data = []
for row in db_result:
data.append(dict(zip(['id', 'count'], row)))
return jsonify(data)
@app.route("/api/songs/stats.csv")
@nocache
# Return data from long_term_stats as csv
def get_stats_csv():
db_result = database.get_long_term_stats()
print(db_result)
csv = "Id,Playbacks\n"
for row in db_result:
csv += str(row[0]) + "," + str(row[1]) + "\n"
return Response(csv, mimetype='text/csv')
@app.route("/api/songs/stats.csv", methods=['POST'])
@nocache @nocache
@basic_auth.required @basic_auth.required
# Update long_term_stats from csv def delete_entry(entry_id):
def update_stats_csv():
if not request.files:
abort(400)
file = request.files['file']
if file.filename is None:
abort(400)
else:
filename = secure_filename(file.filename)
if filename == '':
abort(400)
if not filename.endswith('.csv'):
abort(400)
if file:
if database.import_stats(file):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
else:
abort(400)
@app.route("/api/songs/details/<song_id>")
def get_song_details(song_id):
result = database.get_song_details(song_id)
if result is None:
abort(404)
else:
return jsonify(dict(zip(['karafun_id', 'title', 'artist', 'year', 'duo', 'explicit', 'styles', 'languages'], result[0])))
@app.route("/api/entries/delete/<entry_id>", methods=['GET'])
@nocache
@basic_auth.required
def delete_entry_admin(entry_id):
if database.delete_entry(entry_id):
return Response('{"status": "OK"}', mimetype='text/json')
else:
return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/delete/<entry_id>", methods=['POST'])
@nocache
def delete_entry_user(entry_id):
if not request.json:
print(request.data)
abort(400)
client_id = request.json['client_id']
if not helpers.is_valid_uuid(client_id):
print(request.data)
abort(400)
if database.get_raw_entry(entry_id)[3] != client_id: # type: ignore
print(request.data)
abort(403)
if database.delete_entry(entry_id): if database.delete_entry(entry_id):
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
else: else:
@ -283,7 +158,7 @@ def delete_entries():
return return
updates = database.delete_entries(request.json) updates = database.delete_entries(request.json)
if updates >= 0: if updates >= 0:
return Response('{"status": "OK", "updates": ' + str(updates) + '}', mimetype='text/json') return Response('{"status": "OK", "updates": '+str(updates)+'}', mimetype='text/json')
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400) return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@ -297,7 +172,6 @@ def mark_sung(entry_id):
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json') return Response('{"status": "FAIL"}', mimetype='text/json')
@app.route("/api/entries/mark_transferred/<entry_id>") @app.route("/api/entries/mark_transferred/<entry_id>")
@nocache @nocache
@basic_auth.required @basic_auth.required
@ -312,8 +186,9 @@ def mark_transferred(entry_id):
@nocache @nocache
@basic_auth.required @basic_auth.required
def set_accept_entries(value): def set_accept_entries(value):
global accept_entries
if (value == '0' or value == '1'): if (value == '0' or value == '1'):
helpers.set_accept_entries(app, bool(int(value))) accept_entries = bool(int(value))
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json', status=400) return Response('{"status": "FAIL"}', mimetype='text/json', status=400)
@ -322,24 +197,18 @@ def set_accept_entries(value):
@app.route("/api/entries/accept") @app.route("/api/entries/accept")
@nocache @nocache
def get_accept_entries(): def get_accept_entries():
accept_entries = helpers.get_accept_entries(app) global accept_entries
return Response('{"status": "OK", "value": ' + str(int(accept_entries)) + '}', mimetype='text/json') return Response('{"status": "OK", "value": '+str(int(accept_entries))+'}', mimetype='text/json')
@app.route("/api/event/close") @app.route("/api/played/clear")
@nocache @nocache
@basic_auth.required @basic_auth.required
def close_event(): def clear_played_songs():
try: if database.clear_played_songs():
database.transfer_playbacks()
database.clear_played_songs()
database.delete_all_entries()
helpers.reset_current_event_id(app)
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
except Exception: else:
response = jsonify({"status": "FAIL", "message": "An error occured while closing the event."}) return Response('{"status": "FAIL"}', mimetype='text/json')
response.status_code = 400
return response
@app.route("/api/entries/delete_all") @app.route("/api/entries/delete_all")
@ -347,7 +216,6 @@ def close_event():
@basic_auth.required @basic_auth.required
def delete_all_entries(): def delete_all_entries():
if database.delete_all_entries(): if database.delete_all_entries():
helpers.reset_current_event_id(app)
return Response('{"status": "OK"}', mimetype='text/json') return Response('{"status": "OK"}', mimetype='text/json')
else: else:
return Response('{"status": "FAIL"}', mimetype='text/json') return Response('{"status": "FAIL"}', mimetype='text/json')
@ -359,24 +227,17 @@ def admin():
return redirect("/", code=303) return redirect("/", code=303)
@app.route("/api/events/current") @app.before_first_request
@nocache
def get_current_event():
return Response('{"status": "OK", "event": "' + helpers.get_current_event_id(app) + '"}', mimetype='text/json')
def activate_job(): def activate_job():
with app.app_context(): helpers.load_version(app)
helpers.load_dbconfig(app) helpers.create_data_directory()
helpers.load_version(app) database.create_entry_table()
database.create_schema() database.create_song_table()
database.create_entry_table() database.create_done_song_table()
database.create_song_table() database.create_list_view()
database.create_done_song_table() database.create_done_song_view()
database.create_list_view() helpers.setup_config(app)
database.create_done_song_view()
database.create_config_table()
helpers.setup_config(app)
@app.after_request @app.after_request
@ -385,15 +246,14 @@ def add_header(response):
Add headers to both force latest IE rendering engine or Chrome Frame, Add headers to both force latest IE rendering engine or Chrome Frame,
and also to cache the rendered page for 10 minutes. and also to cache the rendered page for 10 minutes.
""" """
if 'Cache-Control' not in response.headers: if not 'Cache-Control' in response.headers:
response.headers['Cache-Control'] = 'private, max-age=600, no-cache, must-revalidate' response.headers['Cache-Control'] = 'private, max-age=600'
return response return response
@app.context_processor @app.context_processor
def inject_version(): def inject_version():
return dict(karaoqueue_version=app.config['VERSION']) return dict(karaoqueue_version=app.config['VERSION'])
# Perform setup here so it will be executed when the module is imported by the WSGI server. if __name__ == "__main__":
activate_job() app.run(host='127.0.0.1', port=8080, debug=True)

View File

@ -1,5 +1,8 @@
def dict_from_row(row):
return dict(zip(row.keys(), row))
def dict_from_rows(rows): def dict_from_rows(rows):
outlist = [] outlist=[]
for row in rows: for row in rows:
outlist.append(dict(row._mapping)) outlist.append(dict_from_row(row))
return outlist return outlist

View File

@ -1,341 +1,190 @@
# -*- coding: utf_8 -*- # -*- coding: utf_8 -*-
from sqlalchemy import create_engine, engine, text import sqlite3
import pandas import pandas
from io import StringIO from io import StringIO
from flask import current_app
import uuid
song_table = "songs" song_table = "songs"
entry_table = "entries" entry_table = "entries"
index_label = "Id" index_label = "Id"
done_table = "done_songs" done_table = "done_songs"
sql_engine = None
def open_db():
def get_db_engine() -> engine.base.Engine: conn = sqlite3.connect("/tmp/karaoqueue.db")
global sql_engine conn.execute('PRAGMA encoding = "UTF-8";')
if (not sql_engine): return conn
sql_engine = create_engine(
current_app.config.get("DBCONNSTRING")) # type: ignore
return sql_engine
def import_songs(song_csv): def import_songs(song_csv):
print("Start importing Songs...") print("Start importing Songs...")
df = pandas.read_csv(StringIO(song_csv), sep=';') df = pandas.read_csv(StringIO(song_csv), sep=';')
with get_db_engine().connect() as conn: conn = open_db()
df.to_sql(song_table, conn, if_exists='replace', cur = conn.cursor()
index=False) df.to_sql(song_table, conn, if_exists='replace',
try: index=False)
cur = conn.execute(text("ALTER TABLE songs ADD FULLTEXT(Title,Artist)")) cur.execute("SELECT Count(Id) FROM songs")
conn.commit() num_songs = cur.fetchone()[0]
except Exception: conn.close()
pass
cur = conn.execute(text("SELECT Count(Id) FROM songs"))
num_songs = cur.fetchone()[0] # type: ignore
conn.commit()
print("Imported songs ({} in Database)".format(num_songs)) print("Imported songs ({} in Database)".format(num_songs))
return ("Imported songs ({} in Database)".format(num_songs)) return("Imported songs ({} in Database)".format(num_songs))
def import_stats(stats_csv):
print("Start importing Stats...")
df = pandas.read_csv(stats_csv, sep=',')
if (df.columns[0] != "Id" or df.columns[1] != "Playbacks"):
return False
with get_db_engine().connect() as conn:
for index, row in df.iterrows():
stmt = text(
"INSERT INTO long_term_stats (Id,Playbacks) VALUES (:par_id,:par_playbacks) ON DUPLICATE KEY UPDATE Playbacks=:par_playbacks")
conn.execute(stmt, {"par_id": row["Id"], "par_playbacks": row["Playbacks"]})
conn.commit()
return True
def create_schema():
create_song_table()
create_entry_table()
create_done_song_table()
create_config_table()
create_long_term_stats_table()
create_list_view()
create_done_song_view()
def create_entry_table(): def create_entry_table():
with get_db_engine().connect() as conn: conn = open_db()
stmt = text( conn.execute('CREATE TABLE IF NOT EXISTS '+entry_table +
f'CREATE TABLE IF NOT EXISTS `{entry_table}` (ID INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, Song_Id INTEGER NOT NULL, Name VARCHAR(255), Client_Id VARCHAR(36), Transferred INTEGER DEFAULT 0)') ' (ID INTEGER PRIMARY KEY NOT NULL, Song_Id INTEGER NOT NULL, Name VARCHAR(255), Client_Id VARCHAR(36), Transferred INTEGER DEFAULT 0)')
conn.execute(stmt) conn.close()
conn.commit()
def create_done_song_table(): def create_done_song_table():
with get_db_engine().connect() as conn: conn = open_db()
stmt = text( conn.execute('CREATE TABLE IF NOT EXISTS '+done_table +
f'CREATE TABLE IF NOT EXISTS `{done_table}` (Song_Id INTEGER PRIMARY KEY NOT NULL, Plays INTEGER)') ' (Song_Id INTEGER PRIMARY KEY NOT NULL, Plays INTEGER)')
conn.execute(stmt) conn.close()
conn.commit()
def create_song_table(): def create_song_table():
with get_db_engine().connect() as conn: conn = open_db()
stmt = text(f"""CREATE TABLE IF NOT EXISTS `{song_table}` ( conn.execute("CREATE TABLE IF NOT EXISTS \""+song_table+"""\" (
`Id` INTEGER, "Id" INTEGER,
`Title` TEXT, "Title" TEXT,
`Artist` TEXT, "Artist" TEXT,
`Year` VARCHAR(4), "Year" INTEGER,
`Duo` BOOLEAN, "Duo" INTEGER,
`Explicit` INTEGER, "Explicit" INTEGER,
`Date Added` TIMESTAMP, "Date Added" TEXT,
`Styles` TEXT, "Styles" TEXT,
`Languages` TEXT, "Languages" TEXT
PRIMARY KEY (`Id`), )""")
FULLTEXT KEY (`Title`,`Artist`) conn.close()
)""")
conn.execute(stmt)
conn.commit()
def create_long_term_stats_table():
with get_db_engine().connect() as conn:
stmt = text("""CREATE TABLE IF NOT EXISTS `long_term_stats` (
`Id` INTEGER,
`Playbacks` INTEGER,
PRIMARY KEY (`Id`)
)""")
conn.execute(stmt)
conn.commit()
def create_list_view(): def create_list_view():
with get_db_engine().connect() as conn: conn = open_db()
stmt = text("""CREATE OR REPLACE VIEW `Liste` AS conn.execute("""CREATE VIEW IF NOT EXISTS [Liste] AS
SELECT Name, Title, Artist, entries.Id AS entry_ID, songs.Id AS song_ID, entries.Transferred SELECT Name, Title, Artist, entries.Id, songs.Id, entries.Transferred
FROM entries, songs FROM entries, songs
WHERE entries.Song_Id=songs.Id WHERE entries.Song_Id=songs.Id""")
ORDER BY entries.Id ASC conn.close()
""")
conn.execute(stmt)
conn.commit()
def create_done_song_view(): def create_done_song_view():
with get_db_engine().connect() as conn: conn = open_db()
stmt = text("""CREATE OR REPLACE VIEW `Abspielliste` AS conn.execute("""CREATE VIEW IF NOT EXISTS [Abspielliste] AS
SELECT CONCAT(Artist," - ", Title) AS Song, Plays AS Wiedergaben SELECT Artist || \" - \" || Title AS Song, Plays AS Wiedergaben
FROM songs, done_songs FROM songs, done_songs
WHERE done_songs.Song_Id=songs.Id""") WHERE done_songs.Song_Id=songs.Id""")
conn.execute(stmt) conn.close()
conn.commit()
def create_config_table():
with get_db_engine().connect() as conn:
stmt = text("""CREATE TABLE IF NOT EXISTS `config` (
`Key` VARCHAR(50) NOT NULL PRIMARY KEY,
`Value` TEXT
)""")
conn.execute(stmt)
conn.commit()
def get_list(): def get_list():
with get_db_engine().connect() as conn: conn = open_db()
stmt = text("SELECT * FROM Liste") conn.row_factory = sqlite3.Row
cur = conn.execute(stmt) cur = conn.cursor()
cur.execute("SELECT * FROM Liste")
return cur.fetchall() return cur.fetchall()
def get_played_list(): def get_played_list():
with get_db_engine().connect() as conn: conn = open_db()
stmt = text("SELECT * FROM Abspielliste") cur = conn.cursor()
cur = conn.execute(stmt) cur.execute("SELECT * FROM Abspielliste")
return cur.fetchall()
def get_song_suggestions(count: int):
with get_db_engine().connect() as conn:
# Get the top 10 songs with the most plays from the long_term_stats table and join them with the songs table to get the song details.
# Exclude songs that are already in the queue, or in the done_songs table.
stmt = text("""
SELECT s.Id, s.Title, s.Artist, s.Year, s.Duo, s.Explicit, s.Styles, s.Languages
FROM long_term_stats lts
LEFT JOIN songs s ON lts.Id = s.Id
LEFT JOIN entries e ON lts.Id = e.Song_Id
LEFT JOIN done_songs ds ON lts.Id = ds.Song_Id
WHERE e.Id IS NULL AND ds.Song_Id IS NULL
ORDER BY lts.Playbacks DESC
LIMIT :count;
""")
cur = conn.execute(stmt, {"count": count})
return cur.fetchall()
def get_long_term_stats():
with get_db_engine().connect() as conn:
stmt = text("""
SELECT lts.Id, lts.Playbacks
FROM long_term_stats lts
""")
cur = conn.execute(stmt)
return cur.fetchall() return cur.fetchall()
def get_song_list(): def get_song_list():
with get_db_engine().connect() as conn: conn = open_db()
stmt = text("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;") cur = conn.cursor()
cur = conn.execute(stmt) cur.execute("SELECT Artist || \" - \" || Title AS Song, Id FROM songs;")
return cur.fetchall() return cur.fetchall()
def get_song_completions(input_string): def get_song_completions(input_string):
with get_db_engine().connect() as conn: conn = open_db()
prepared_string = f"{input_string}" cur = conn.cursor()
prepared_string_with_wildcard = f"%{input_string}%" # Don't look, it burns...
stmt = text( prepared_string = "%{0}%".format(
""" input_string).upper() # "Test" -> "%TEST%"
SELECT CONCAT(Artist, ' - ', Title) AS Song, Id FROM songs print(prepared_string)
WHERE MATCH(Artist, Title) cur.execute(
AGAINST (:prepared_string IN NATURAL LANGUAGE MODE) "SELECT Title || \" - \" || Artist AS Song, Id FROM songs WHERE REPLACE(REPLACE(REPLACE(REPLACE(UPPER( SONG ),'ö','Ö'),'ü','Ü'),'ä','Ä'),'ß','') LIKE (?) LIMIT 20;", (prepared_string,))
LIMIT 20; return cur.fetchall()
""")
cur = conn.execute(
stmt, {"prepared_string": prepared_string, "prepared_string_with_wildcard": prepared_string_with_wildcard}) # type: ignore
return cur.fetchall()
def get_songs_with_details(input_string: str):
with get_db_engine().connect() as conn:
prepared_string = f"%{input_string}"
stmt = text(
"""
SELECT Id, Title, Artist, Year, Duo, Explicit, Styles, Languages FROM songs
WHERE MATCH(Artist, Title)
AGAINST (:prepared_string IN NATURAL LANGUAGE MODE)
LIMIT 20;
"""
)
cur = conn.execute(
stmt, {"prepared_string": prepared_string})
return cur.fetchall()
def get_song_details(song_id: int):
with get_db_engine().connect() as conn:
stmt = text(
"""
SELECT Id, Title, Artist, Year, Duo, Explicit, Styles, Languages FROM songs
WHERE Id = :song_id;
"""
)
cur = conn.execute(
stmt, {"song_id": song_id})
return cur.fetchall()
def add_entry(name, song_id, client_id): def add_entry(name, song_id, client_id):
with get_db_engine().connect() as conn: conn = open_db()
stmt = text( cur = conn.cursor()
"INSERT INTO entries (Song_Id,Name,Client_Id) VALUES (:par_song_id,:par_name,:par_client_id) RETURNING entries.ID;") cur.execute(
cur = conn.execute(stmt, {"par_song_id": song_id, "par_name": name, "INSERT INTO entries (Song_Id,Name,Client_Id) VALUES(?,?,?);", (song_id, name, client_id))
"par_client_id": client_id}) # type: ignore conn.commit()
conn.commit() conn.close()
return cur.fetchone()[0] # type: ignore return
def add_sung_song(entry_id): def add_sung_song(entry_id):
with get_db_engine().connect() as conn: conn = open_db()
stmt = text("SELECT Song_Id FROM entries WHERE Id=:par_entry_id") cur = conn.cursor()
cur = conn.execute(stmt, {"par_entry_id": entry_id}) # type: ignore cur.execute("""SELECT Song_Id FROM entries WHERE Id=?""", (entry_id,))
song_id = cur.fetchone()[0] # type: ignore song_id = cur.fetchone()[0]
stmt = text( cur.execute("""INSERT OR REPLACE INTO done_songs (Song_Id, Plays)
"INSERT INTO done_songs (Song_Id,Plays) VALUES (:par_song_id,1) ON DUPLICATE KEY UPDATE Plays=Plays + 1;") VALUES("""+str(song_id)+""",
conn.execute(stmt, {"par_song_id": song_id}) # type: ignore COALESCE(
conn.commit() (SELECT Plays FROM done_songs
delete_entry(entry_id) WHERE Song_Id="""+str(song_id)+"), 0) + 1)"
)
conn.commit()
delete_entry(entry_id)
conn.close()
return True return True
def toggle_transferred(entry_id): def toggle_transferred(entry_id):
with get_db_engine().connect() as conn: conn = open_db()
cur = conn.execute(text("SELECT Transferred FROM entries WHERE ID = :par_entry_id"), cur = conn.cursor()
{"par_entry_id": entry_id}) # type: ignore cur.execute("SELECT Transferred FROM entries WHERE ID =?", (entry_id,))
marked = cur.fetchall()[0][0] marked = cur.fetchall()[0][0]
if (marked == 0): if(marked == 0):
conn.execute(text("UPDATE entries SET Transferred = 1 WHERE ID = :par_entry_id"), cur.execute(
{"par_entry_id": entry_id}) # type: ignore "UPDATE entries SET Transferred = 1 WHERE ID =?", (entry_id,))
else: else:
conn.execute(text("UPDATE entries SET Transferred = 0 WHERE ID = :par_entry_id"), cur.execute(
{"par_entry_id": entry_id}) # type: ignore "UPDATE entries SET Transferred = 0 WHERE ID =?", (entry_id,))
conn.commit() conn.commit()
conn.close()
return True return True
def check_entry_quota(client_id): def check_entry_quota(client_id):
with get_db_engine().connect() as conn: conn = open_db()
cur = conn.execute(text("SELECT Count(*) FROM entries WHERE entries.Client_Id = :par_client_id"), cur = conn.cursor()
{"par_client_id": client_id}) # type: ignore cur.execute(
"SELECT Count(*) FROM entries WHERE entries.Client_Id = ?", (client_id,))
return cur.fetchall()[0][0] return cur.fetchall()[0][0]
def check_queue_length(): def check_queue_length():
with get_db_engine().connect() as conn: conn = open_db()
cur = conn.execute(text("SELECT Count(*) FROM entries")) cur = conn.cursor()
cur.execute("SELECT Count(*) FROM entries")
return cur.fetchall()[0][0] return cur.fetchall()[0][0]
def transfer_playbacks():
with get_db_engine().connect() as conn:
# Use SQL to update the long_term_stats table. Add the playbacks of the songs in the done_songs table to the playbacks of the songs in the long_term_stats table.
stmt = text("""
INSERT INTO long_term_stats(Id, Playbacks)
SELECT ds.Song_Id, ds.Plays
FROM done_songs ds
LEFT JOIN long_term_stats lts ON ds.Song_Id = lts.Id
ON DUPLICATE KEY
UPDATE Playbacks = lts.Playbacks + VALUES(Playbacks);
""")
result = conn.execute(stmt)
print(result)
conn.commit()
return True
def clear_played_songs(): def clear_played_songs():
with get_db_engine().connect() as conn: conn = open_db()
conn.execute(text("DELETE FROM done_songs")) cur = conn.cursor()
conn.commit() cur.execute("DELETE FROM done_songs")
conn.commit()
conn.close()
return True return True
def get_entry(id):
try:
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT * FROM Liste WHERE entry_ID = :par_id"),
{"par_id": id}) # type: ignore
return cur.fetchall()[0]
except Exception:
return None
def get_raw_entry(id):
try:
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT * FROM entries WHERE ID = :par_id"),
{"par_id": id}) # type: ignore
return cur.fetchall()[0]
except Exception:
return None
def delete_entry(id): def delete_entry(id):
with get_db_engine().connect() as conn: conn = open_db()
conn.execute(text("DELETE FROM entries WHERE id= :par_id"), { cur = conn.cursor()
"par_id": id}) # type: ignore cur.execute("DELETE FROM entries WHERE id=?", (id,))
conn.commit() conn.commit()
conn.close()
return True return True
@ -344,75 +193,20 @@ def delete_entries(ids):
for x in ids: for x in ids:
idlist.append((x,)) idlist.append((x,))
try: try:
with get_db_engine().connect() as conn: conn = open_db()
cur = conn.execute(text("DELETE FROM entries WHERE id= :par_id"), { cur = conn.cursor()
"par_id": idlist}) cur.executemany("DELETE FROM entries WHERE id=?", idlist)
conn.commit() conn.commit()
conn.close()
return cur.rowcount return cur.rowcount
except Exception: except sqlite3.Error as error:
return -1 return -1
def delete_all_entries() -> bool: def delete_all_entries():
with get_db_engine().connect() as conn: conn = open_db()
conn.execute(text("DELETE FROM entries")) cur = conn.cursor()
conn.commit() cur.execute("DELETE FROM entries")
conn.commit()
conn.close()
return True return True
def get_config(key: str) -> str:
try:
with get_db_engine().connect() as conn:
cur = conn.execute(
text("SELECT `Value` FROM config WHERE `Key`= :par_key"), {"par_key": key}) # type: ignore
conn.commit()
return cur.fetchall()[0][0]
except IndexError:
return ""
def set_config(key: str, value: str) -> bool:
with get_db_engine().connect() as conn:
conn.execute(text(
"INSERT INTO config (`Key`, `Value`) VALUES ( :par_key , :par_value) ON DUPLICATE KEY UPDATE `Value`= :par_value"),
{"par_key": key, "par_value": value}
) # type: ignore
conn.commit()
return True
def get_config_list() -> dict:
with get_db_engine().connect() as conn:
cur = conn.execute(text("SELECT * FROM config"))
result_dict = {}
for row in cur.fetchall():
result_dict[row[0]] = row[1]
return result_dict
def check_config_table() -> bool:
with get_db_engine().connect() as conn:
if conn.dialect.has_table(conn, 'config'):
# type: ignore
# type: ignore
if (conn.execute(text("SELECT COUNT(*) FROM config")).fetchone()[0] > 0): # type: ignore
return True
else:
return False
else:
return False
def init_event_id() -> bool:
if not get_config("EventID"):
set_config("EventID", str(uuid.uuid4()))
return True
def reset_event_id() -> bool:
set_config("EventID", str(uuid.uuid4()))
return True
def get_event_id() -> str:
return get_config("EventID")

11
backend/docker/Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM tiangolo/uwsgi-nginx-flask:python3.7
RUN pip install requests
RUN pip install pandas
RUN pip install Flask-BasicAuth
RUN pip install bs4
COPY ../app /app

View File

@ -1,27 +1,30 @@
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import subprocess import json
import os import os
import uuid import uuid
from flask import make_response, Flask from flask import make_response
from functools import wraps, update_wrapper from functools import wraps, update_wrapper
from datetime import datetime from datetime import datetime
import database
data_directory = "data"
config_file = data_directory+"/config.json"
def create_data_directory():
if not os.path.exists(data_directory):
os.makedirs(data_directory)
def get_catalog_url(): def get_catalog_url():
r = requests.get('https://www.karafun.de/karaoke-song-list.html') r = requests.get('https://www.karafun.de/karaoke-song-list.html')
soup = BeautifulSoup(r.content, 'html.parser') soup = BeautifulSoup(r.content, 'html.parser')
url = soup.findAll( url = soup.findAll('a', href=True, text='Verfügbar in CSV-Format')[0]['href']
'a', href=True, text='Verfügbar in CSV-Format')[0]['href']
return url return url
def get_songs(url): def get_songs(url):
r = requests.get(url) r = requests.get(url)
return r.text return r.text
def is_valid_uuid(val): def is_valid_uuid(val):
try: try:
uuid.UUID(str(val)) uuid.UUID(str(val))
@ -29,147 +32,48 @@ def is_valid_uuid(val):
except ValueError: except ValueError:
return False return False
def check_config_exists(): def check_config_exists():
return database.check_config_table() return os.path.isfile(config_file)
def load_version(app):
def load_version(app: Flask):
if app.config['DEBUG'] is True:
app.config['VERSION'] = subprocess.Popen("echo \"$(git rev-parse --abbrev-ref HEAD)-$(git describe)\"", shell=True, stdout=subprocess.PIPE).stdout.read().decode('utf-8').strip() + " (debug)" # noqa: E501 # type: ignore
return
if os.environ.get("SOURCE_VERSION"): if os.environ.get("SOURCE_VERSION"):
app.config['VERSION'] = os.environ.get("SOURCE_VERSION") # type: ignore # noqa: E501 app.config['VERSION'] = os.environ.get("SOURCE_VERSION")[0:7]
return elif os.path.isfile(".version"):
else: with open('.version', 'r') as file:
app.config['VERSION'] = "Unknown" data = file.read().replace('\n', '')
if data:
app.config['VERSION'] = data
def load_dbconfig(app: Flask):
if os.environ.get("FLASK_ENV") == "development":
app.config['DBCONNSTRING'] = os.environ.get("DBSTRING")
else:
if os.environ.get("DEPLOYMENT_PLATFORM") == "Heroku":
if os.environ.get("JAWSDB_MARIA_URL"):
app.config['DBCONNSTRING'] = os.environ.get("JAWSDB_MARIA_URL")
else: else:
app.config['DBCONNSTRING'] = "" app.config['VERSION'] = ""
if os.environ.get("DEPLOYMENT_PLATFORM") == "Docker": else:
if os.environ.get("DBSTRING"): app.config['VERSION'] = ""
app.config['DBCONNSTRING'] = os.environ.get("DBSTRING")
else:
app.config['DBCONNSTRING'] = ""
elif os.path.isfile(".dbconn"):
with open('.dbconn', 'r') as file:
data = file.read().replace('\n', '')
if data:
app.config['DBCONNSTRING'] = data
else:
app.config['DBCONNSTRING'] = ""
else:
exit("""No database connection string found. Cannot continue.
Please set the environment variable DBSTRING or
create a file .dbconn in the root directory of the project.""")
# Check if config exists in DB, if not, create it. def setup_config(app):
if check_config_exists():
config = json.load(open(config_file))
def setup_config(app: Flask): with open(config_file, 'r') as handle:
if check_config_exists() is False: config = json.load(handle)
print("No config found, creating new config") print("Loaded existing config")
initial_username = os.environ.get("INITIAL_USERNAME") else:
initial_password = os.environ.get("INITIAL_PASSWORD") config = {'username': 'admin', 'password': 'changeme', 'entryquota': 3, 'maxqueue': 20}
if initial_username is None: with open(config_file, 'w') as handle:
print( json.dump(config, handle, indent=4, sort_keys=True)
"No initial username set. Please set the environment variable INITIAL_USERNAME") print("Wrote new config")
exit()
if initial_password is None:
print(
"No initial password set. Please set the environment variable INITIAL_PASSWORD")
exit()
default_config = {'username': initial_username,
'password': initial_password,
'entryquota': 2,
'maxqueue': 10,
'entries_allowed': 1,
'theme': 'default.css'}
for key, value in default_config.items():
database.set_config(key, value)
print("Created new config")
database.init_event_id()
config = database.get_config_list()
app.config['BASIC_AUTH_USERNAME'] = config['username'] app.config['BASIC_AUTH_USERNAME'] = config['username']
app.config['BASIC_AUTH_PASSWORD'] = config['password'] app.config['BASIC_AUTH_PASSWORD'] = config['password']
app.config['ENTRY_QUOTA'] = config['entryquota'] app.config['ENTRY_QUOTA'] = config['entryquota']
app.config['MAX_QUEUE'] = config['maxqueue'] app.config['MAX_QUEUE'] = config['maxqueue']
app.config['ENTRIES_ALLOWED'] = bool(config['entries_allowed'])
app.config['THEME'] = config['theme']
app.config['EVENT_ID'] = database.get_event_id()
# set queue admittance
def set_accept_entries(app: Flask, allowed: bool):
if allowed:
app.config['ENTRIES_ALLOWED'] = True
database.set_config('entries_allowed', '1')
else:
app.config['ENTRIES_ALLOWED'] = False
database.set_config('entries_allowed', '0')
# get queue admittance
def get_accept_entries(app: Flask) -> bool:
state = bool(int(database.get_config('entries_allowed')))
app.config['ENTRIES_ALLOWED'] = state
return state
# Write settings from current app.config to DB
def persist_config(app: Flask):
config = {'username': app.config['BASIC_AUTH_USERNAME'], 'password': app.config['BASIC_AUTH_PASSWORD'],
'entryquota': app.config['ENTRY_QUOTA'], 'maxqueue': app.config['MAX_QUEUE']}
for key, value in config.items():
database.set_config(key, value)
# Get available themes from themes directory
def get_themes():
themes = []
for theme in os.listdir('./static/css/themes'):
themes.append(theme)
return themes
# Set theme
def set_theme(app: Flask, theme: str):
if theme in get_themes():
app.config['THEME'] = theme
database.set_config('theme', theme)
else:
print("Theme not found, not setting theme.")
def get_current_event_id(app: Flask):
return app.config['EVENT_ID']
def reset_current_event_id(app: Flask):
database.reset_event_id()
app.config['EVENT_ID'] = database.get_event_id()
def nocache(view): def nocache(view):
@wraps(view) @wraps(view)
def no_cache(*args, **kwargs): def no_cache(*args, **kwargs):
response = make_response(view(*args, **kwargs)) response = make_response(view(*args, **kwargs))
response.headers['Last-Modified'] = datetime.now() # type: ignore response.headers['Last-Modified'] = datetime.now()
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0' response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
response.headers['Pragma'] = 'no-cache' response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '-1' response.headers['Expires'] = '-1'
return response return response
return update_wrapper(no_cache, view) return update_wrapper(no_cache, view)

View File

@ -1,36 +1,5 @@
autopep8~=2.0.2 requests
beautifulsoup4~=4.12.0 pandas
bs4~=0.0.1 Flask-BasicAuth
certifi~=2022.12.7 bs4
charset-normalizer~=3.1.0 gunicorn
click~=8.1.3
flake8~=6.0.0
Flask~=2.3.2
Flask-BasicAuth~=0.2.0
greenlet~=2.0.2
gunicorn~=20.1.0
idna~=3.4
itsdangerous~=2.1.2
Jinja2~=3.1.2
mariadb~=1.1.6
MarkupSafe~=2.1.2
mccabe~=0.7.0
mysql~=0.0.3
mysqlclient~=2.1.1
numpy~=1.24.2
packaging~=23.0
pandas~=1.5.3
pycodestyle~=2.10.0
pyflakes~=3.0.1
PyMySQL~=1.0.3
python-dateutil~=2.8.2
pytz~=2023.3
requests~=2.31.0
six~=1.16.0
soupsieve~=2.4
SQLAlchemy~=2.0.7
toml~=0.10.2
tomli~=2.0.1
typing_extensions~=4.5.0
urllib3~=1.26.15
Werkzeug~=3.0.0

View File

@ -1,238 +1,77 @@
:root { body {
/* Navbar */ padding-top: 5rem;
--navbar-background-color: #343a40;
--navbar-text-color: rgba(255, 255, 255, .5);
--navbar-text-color-hover: rgba(255, 255, 255, .75);
--navbar-text-color-active: rgba(255, 255, 255, 1);
--navbar-padding: 4.5rem;
/* Common */
--background-color: #ffffff;
--background-color-var: #f5f5f5;
--text-color: #212529;
--text-color-var: #343a40;
--text-color-light: #6c757d;
/* Modals */
--modal-background-color: #ffffff;
--modal-separator-color: #dee2e6;
--modal-close-color: #212529;
/* Tables */
--table-border-color: #dee2e6;
/* Input */
--input-background-color: #ffffff;
/* Misc */
--copy-highlight-color: rgba(251, 255, 0, 0.6);
/* Toasts */
--toast-background-color: #ffffff;
--toast-text-color: #212529;
/* Footer */
--footer-height: 80px;
} }
html, body {
body { height: 100%;
padding-top: var(--navbar-padding);
background-color: var(--background-color);
}
html,
body {
height: 100%;
} }
.site { .site {
min-height: calc(100vh - var(--navbar-padding) - var(--footer-height)); height: auto;
margin-bottom: -var(--footer-height); min-height: 100%;
}
main {
padding-bottom: 60px; /* Höhe des Footers */
} }
.footer { .footer {
/*margin-top: var(--footer-height);*/ margin-top: -60px;
width: 100%; width: 100%;
height: var(--footer-height); height: 60px;
/* Set the fixed height of the footer here */
background-color: var(--background-color-var); /*line-height: 60px; /* Vertically center the text there */
background-color: #f5f5f5;
} }
.topbutton { .topbutton {
width: 100%; width: 100%;
} }
table td { table td {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
table.entries tbody tr[data-index="0"] { table.entries tbody tr[data-index="0"] {
background-color: #007bff80; background-color: #007bff80;
font-weight: 600; font-weight: 600;
} }
table.entries tbody tr[data-index="1"] { table.entries tbody tr[data-index="1"] {
background-color: #007bff40; background-color: #007bff40;
font-weight: 500; font-weight: 500;
} }
table.entries tbody tr[data-index="2"] { table.entries tbody tr[data-index="2"] {
background-color: #007bff20; background-color: #007bff20;
font-weight: 400; font-weight: 400;
} }
table.entries tbody tr[data-index="3"] { table.entries tbody tr[data-index="3"] {
background-color: #007bff10; background-color: #007bff10;
} }
table td:first-child { table td:first-child {
max-width: 200px !important; max-width: 200px !important;
}
.fa-solid {
vertical-align: auto;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.topbutton { .topbutton {
width: auto; width: auto;
} }
} }
@media print { @media print {
body { body {
font-size: 1.3em; font-size: 1.3em;
} }
.footer { .footer {
display: none !important; display: none !important;
} }
.admincontrols { .admincontrols {
display: none; display: none;
} }
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
.footer {
background-color: var(--background-color-var);
}
.modal-content {
background-color: var(--background-color);
color: var(--text-color);
}
.modal-header {
background-color: var(--background-color);
color: var(--text-color-var);
border-color: var(--modal-separator-color);
}
.modal-footer {
background-color: var(--background-color);
color: var(--text-color-var);
border-color: var(--modal-separator-color);
}
.form-control {
background-color: var(--input-background-color);
color: var(--text-color)
}
.form-control:focus {
background-color: var(--input-background-color);
color: var(--text-color)
}
.table td,
.table th {
border-color: var(--table-border-color)
}
.table thead th {
border-color: var(--table-border-color)
}
table td.buttoncell {
text-align: end;
}
.close {
color: var(--text-color)
}
pre {
color: var(--text-color-var)
}
#entrytable td>span:hover {
background-color: var(--copy-highlight-color);
}
.list-indicator {
width: 1.5rem;
height: 1.5rem;
}
#songYear {
font-size: small;
font-weight: 400;
color: var(--text-color-light);
}
.construction_bg {
background: repeating-linear-gradient(45deg,
#222200,
#222200 10px,
#000000 10px,
#000000 20px) !important;
}
.toast {
background-color: var(--toast-background-color);
color: var(--toast-text-color);
}
.toast-header {
background-color: var(--toast-background-color);
color: var(--toast-text-color);
}
@media (prefers-color-scheme: dark) {
:root {
/* Navbar */
--navbar-background-color: #343a40;
--navbar-text-color: rgba(255, 255, 255, .5);
--navbar-text-color-hover: rgba(255, 255, 255, .75);
--navbar-text-color-active: rgba(255, 255, 255, 1);
/* Common */
--background-color: #121212;
--background-color-var: #232323;
--text-color: #f5f5f5;
--text-color-var: #a2a2a2;
--text-color-light: #6c757d;
/* Modals */
--modal-background-color: #121212;
--modal-separator-color: #232323;
--modal-close-color: #f5f5f5;
/* Tables */
--table-border-color: #232323;
/* Input */
--input-background-color: #343434;
/* Toasts */
--toast-background-color: #232323;
--toast-text-color: #f5f5f5;
}
} }

View File

@ -1,91 +0,0 @@
.navbar {
background: #090a28 !important;
}
.navbar .navbar-toggle:hover,
.navbar .navbar-toggle:focus {
background-color: #900000 !important;
}
.navbar .navbar-toggle {
border: none;
}
.navbar .navbar-nav>.open>a,
.navbar .navbar-nav>.open>a:hover,
.navbar .navbar-nav>.open>a:focus {
color: #CF2323 !important;
background-color: #050515 !important;
}
.btn-primary {
background-color: #15175b;
border-color: #15175b;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #0e103e;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #0e103e;
border-color: #0e103e;
}
.btn-primary.disabled,
.btn-primary:disabled,
.btn-primary[disabled] {
background-color: #0e103e;
background-image: none;
}
.dropdown-menu>li>a:hover,
.dropdown-menu>li>a:focus {
color: #8A0711;
}
.dropdown-menu>.active>a,
.dropdown-menu>.active>a:hover,
.dropdown-menu>.active>a:focus {
background-color: #2e6da4;
background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4));
background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);
background-repeat: repeat-x;
}
.navbar .navbar-nav>li>a:hover {
color: #b60000;
}
.form-control:focus {
border-color: rgba(21, 23, 91, 0.8);
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075),
0 0 8px rgba(21, 23, 91, 0.6);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset,
0 0 8px rgba(21, 23, 91, 0.6);
}
a {
color: #900000;
}
a:hover,
a:focus {
color: #670000;
}
.navbar-brand {
display: flex;
align-items: center;
}
.navbar-brand > img {
height: 4rem;
}

View File

@ -6,39 +6,28 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
<meta name="color-scheme" content="light dark" />
<link rel="icon" href="favicon.ico"> <link rel="icon" href="favicon.ico">
<link rel="manifest" href="/static/manifest.webmanifest"> <link rel="manifest" href="/static/manifest.webmanifest">
<title>{% block title %}{% endblock %} - KaraoQueue</title> <title>{% block title %}{% endblock %} - KaraoQueue</title>
<!-- Bootstrap-Tables --> <!-- Bootstrap-Tables -->
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.21.2/dist/bootstrap-table.min.css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.css">
<!-- Bootstrap-Toaster-->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/css/bootstrap-toaster.min.css"
integrity="sha512-kYPLvO+Bu+xttOhbQvxs9nx7XSdxrb2JexRxQ3CpJQ7EtmlkBsWyOjlinLgiLWeLxuupFYB4cPqLOo0gnBnzeQ=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/css/bootstrap.min.css" <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous"> integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<!-- Custom styles for this template --> <!-- Custom styles for this template -->
<link href="static/css/style.css" rel="stylesheet"> <link href="static/css/style.css" rel="stylesheet">
<!-- Fontawesome Icons --> <!-- Fontawesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css"
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Bootstraptoggle --> <!-- Bootstraptoggle -->
<link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet"> <link href="https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css" rel="stylesheet">
<!-- Active Theme -->
<link href="static/css/themes/{{config['THEME']}}" rel="stylesheet">
</head> </head>
<body> <body>
@ -79,53 +68,46 @@
</main><!-- /.container --> </main><!-- /.container -->
</div> </div>
<!-- Footer --> <!-- Footer -->
<footer class="footer {% if debug %} construction_bg {% endif %}"> <footer class="footer">
<div class="container text-center py-3"> <div class="container text-center py-3">
{% if not auth %} {% if not auth %}
<a href="/login" class="ml-1 mr-1"><i class="fas fa-sign-in-alt mr-1"></i><span>Login</span></a> <a href="/login" class="ml-1 mr-1"><i class="fas fa-sign-in-alt mr-1"></i><span>Login</span></a>
{% endif %} {% endif %}
<a href="https://github.com/PhoenixTwoFive/karaoqueue" class="ml-1 mr-1"><i <!--<a href="https://github.com/PhoenixTwoFive/karaoqueue"
class="fab fa-github mr-1"></i><span>Github</span></a> class="ml-1 mr-1"><i class="fab fa-github mr-1"></i><span>Github</span></a>-->
<span class="text-muted"> {{karaoqueue_version}} -&nbsp;2019-23 - <span id="myName">Phillip <span class="text-muted">KaraoQueue {{karaoqueue_version}} -&nbsp;<span>&copy</span>&nbsp;2019-21 - Phillip
Kühne</span></span> Kühne</span>
</div> </div>
</footer> </footer>
<!-- Footer --> <!-- Footer -->
<!-- Bootstrap core JavaScript <!-- Bootstrap core JavaScript
================================================== --> ================================================== -->
<!-- Placed at the end of the document so the pages load faster --> <!-- Placed at the end of the document so the pages load faster -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js" <script src="https://code.jquery.com/jquery-3.2.1.min.js"
integrity="sha512-pumBsjNRGGqkPzKHndZMaAG+bir374sORyzM3uulLV14lN5LyykqNk8eEeUlUkB3U0M4FApyaHraT65ihJhDpQ==" integrity="sha384-xBuQ/xzmlsLoJpyjoggmTEz8OWUFM0/RC5BsqQBDX2v5cMvDHcMakNTNrHIW2I5f" crossorigin="anonymous">
crossorigin="anonymous" referrerpolicy="no-referrer"></script> </script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.14.6/dist/umd/popper.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous">
crossorigin="anonymous"></script> </script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.2.1/dist/js/bootstrap.min.js" <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous">
crossorigin="anonymous"></script> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.4.0/bootbox.min.js"
integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous"> integrity="sha256-4F7e4JsAJyLUdpP7Q8Sah866jCOhv72zU5E8lIRER4w=" crossorigin="anonymous">
</script> </script>
<script src="https://unpkg.com/bootstrap-table@1.21.2/dist/bootstrap-table.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.js"></script>
<script <script
src="https://unpkg.com/bootstrap-table@1.21.2/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"></script> src="https://unpkg.com/bootstrap-table@1.15.3/dist/extensions/auto-refresh/bootstrap-table-auto-refresh.min.js"></script>
<script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script> <script src="https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-toaster/4.1.2/js/bootstrap-toaster.min.js"
integrity="sha512-Ur6jgeoP3jnn38C7oBzDqMLRb+wxG2PXLKqgx2vgQ1ePFvbJ28f9iQSJplHD0APFHELOeS/df+RPNeENFtLrYw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
{% block extrajs %}{% endblock %} {% block extrajs %}{% endblock %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
checkEventID() loadOrGenerateClientId()
// get current URL path and assign 'active' class // get current URL path and assign 'active' class
var pathname = window.location.pathname; var pathname = window.location.pathname;
$('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active'); $('.navbar-nav > li > a[href="' + pathname + '"]').parent().addClass('active');
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
setTimeout(function () {
$('#myName').text("Klack-Klack")
}, Math.floor(Math.random() * 20000) + 20000);
}) })
function create_UUID() { function create_UUID() {
@ -140,53 +122,9 @@
function loadOrGenerateClientId() { function loadOrGenerateClientId() {
if (!localStorage.getItem("clientId")) { if (!localStorage.getItem("clientId")) {
localStorage.setItem("clientId", create_UUID()) localStorage.setItem("clientId",create_UUID())
} }
} }
function getClientId() {
return localStorage.getItem("clientId")
}
async function checkEventID() {
const localEventID = localStorage.getItem("eventID")
const resp = await fetch("/api/events/current")
const respJson = await resp.json()
const remoteEventID = respJson.event
if (localEventID == null || localEventID != remoteEventID) {
localStorage.clear()
localStorage.setItem("eventID", remoteEventID)
loadOrGenerateClientId()
}
}
function addEntry(entryId) {
entryArray = JSON.parse(localStorage.getItem("ownedEntries"))
if (entryArray == null) {
entryArray = []
}
entryArray.push(entryId)
localStorage.setItem("ownedEntries", JSON.stringify(entryArray))
}
function removeEntry(entryId) {
entryArray = JSON.parse(localStorage.getItem("ownedEntries"))
if (entryArray == null) {
entryArray = []
}
entryArray = entryArray.filter(function (value, index, arr) { return value != entryId; });
localStorage.setItem("ownedEntries", JSON.stringify(entryArray))
}
function getOwnedEntries() {
var entries = JSON.parse(localStorage.getItem("ownedEntries"))
if (entries == null) {
entries = []
}
return entries;
}
</script> </script>
</body> </body>

View File

@ -1,3 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Warteliste{% endblock %} {% block title %}Warteliste{% endblock %}
{% block content %} {% block content %}
@ -7,7 +9,7 @@
data-url="/api/queue" data-url="/api/queue"
data-pagination="true" data-pagination="true"
data-classes="table" data-classes="table"
data-show-refresh="false" data-show-refresh="false"
data-auto-refresh="true" data-auto-refresh="true"
data-auto-refresh-interval="10"> data-auto-refresh-interval="10">
<thead> <thead>
@ -15,7 +17,6 @@
<th data-field="Name">Name</th> <th data-field="Name">Name</th>
<th data-field="Title">Song</th> <th data-field="Title">Song</th>
<th data-field="Artist">Künstler</th> <th data-field="Artist">Künstler</th>
<th scope="col" data-formatter="TableActionsFormatter"></th>
</tr> </tr>
</thead> </thead>
</table> </table>
@ -23,51 +24,12 @@
{% endblock %} {% endblock %}
{% block extrajs %} {% block extrajs %}
<script> <script>
$.getJSON("/api/entries/accept", (data) => { $.getJSON("/api/entries/accept", (data) => {
if (data["value"] == 0) { if (data["value"]==0) {
$("#bfb").addClass("disabled") $("#bfb").addClass("disabled")
$("#bfb").prop("aria-disabled", true); $("#bfb").prop("aria-disabled",true);
$("#bfb").prop("tabindex", "-1"); $("#bfb").prop("tabindex","-1");
$("#bfb").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='bottom'></span>");
$(".tooltip-span").prop("title", "Eintragungen sind leider momentan nicht möglich.")
$('[data-toggle="tooltip"]').tooltip()
}
})
function TableActionsFormatter(value, row, index) {
if (getOwnedEntries().includes(row.entry_ID)) {
return "<button type='button' class='btn btn-danger' data-toggle='tooltip' data-placement='top' title='Eintrag zurückziehen' onclick=\"event.stopPropagation();$(this).tooltip('dispose');requestDeletionAsUser(" + row["entry_ID"] + ")\"><i class='fas fa-trash'></i></button>"
}
return ""
} }
})
function requestDeletionAsUser(id) {
bootbox.confirm("Wirklich den Eintrag zurückziehen? Das könnte zu einer langen Wartezeit führen!", function (result) {
if (result) {
payload = {
"client_id": localStorage.getItem("clientId"),
"entry_id": id
}
$.ajax({
url: "/api/entries/delete/" + id,
type: "POST",
data: JSON.stringify(payload),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
toast = {
title: "Erfolgreich zurückgezogen",
message: "Eintrag wurde gelöscht",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
location.reload()
}
})
}
})
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,13 +1,15 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Warteliste-Admin{% endblock %} {% block title %}Warteliste-Admin{% endblock %}
{% block content %} {% block content %}
<style> <style>
table td:nth-child(2) { table td:nth-child(2) {
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto; overflow-x: auto;
text-overflow: clip; text-overflow: clip;
max-width: 200px !important; max-width: 200px !important;
} }
</style> </style>
<div class="container"> <div class="container">
<div id="toolbar"> <div id="toolbar">
@ -16,19 +18,30 @@
<button type="button" class="topbutton btn btn-danger" onclick="confirmUpdateSongDatabase()"><i <button type="button" class="topbutton btn btn-danger" onclick="confirmUpdateSongDatabase()"><i
class="fas fa-file-import mr-2"></i>Song-Datenbank class="fas fa-file-import mr-2"></i>Song-Datenbank
aktualisieren</button> aktualisieren</button>
<input id="entryToggle" type="checkbox" class="topbutton" data-toggle="toggle" data-on="Eintragen erlaubt" <input id="entryToggle" type="checkbox" class="topbutton" data-toggle="toggle" data-on="Eintragen erlaubt" data-off="Eintragen deaktiviert" data-onstyle="success" data-offstyle="danger">
data-off="Eintragen deaktiviert" data-onstyle="success" data-offstyle="danger">
</div> </div>
<table class="table entries" id="entrytable" data-toggle="table" data-search="true" data-show-columns="true" <table class="table entries"
data-show-toggle="true" data-multiple-select-row="true" data-click-to-select="true" data-toolbar="#toolbar" id="entrytable"
data-pagination="true" data-show-extended-pagination="true" data-classes="table table-hover" data-toggle="table"
data-url="/api/queue" data-show-refresh="true" data-auto-refresh="true" data-auto-refresh-interval="10"> data-search="true"
data-show-columns="true"
data-show-toggle="true"
data-multiple-select-row="true"
data-click-to-select="true"
data-toolbar="#toolbar"
data-pagination="true"
data-show-extended-pagination="true"
data-classes="table table-hover"
data-url="/api/queue"
data-show-refresh="true"
data-auto-refresh="true"
data-auto-refresh-interval="10">
<thead> <thead>
<tr> <tr>
<th data-field="state" data-checkbox="true"></th> <th data-field="state" data-checkbox="true"></th>
<th scope="col" data-field="Name" data-formatter="CopyFormatter">Name</th> <th scope="col" data-field="Name">Name</th>
<th scope="col" data-field="Title" data-formatter="CopyFormatter">Song</th> <th scope="col" data-field="Title">Song</th>
<th scope="col" data-field="Artist" data-formatter="CopyFormatter">Künstler</th> <th scope="col" data-field="Artist">Künstler</th>
<th scope="col" data-formatter="TableActions">Aktionen</th> <th scope="col" data-formatter="TableActions">Aktionen</th>
</tr> </tr>
</thead> </thead>
@ -39,19 +52,16 @@
{% block extrajs %} {% block extrajs %}
<script> <script>
$(function () { $(function () {
$('#entryToggle').change(function() {
$.ajax({url: "/api/entries/accept/"+($('#entryToggle').is(":checked") ? "1" : "0"), complete: setTimeout(refreshEntryToggle, 1000)});
})
refreshEntryToggle() refreshEntryToggle()
$('#entryToggle').change(function () { $("#entrytable").bootstrapTable().on('load-success.bs.table', function() {
$.ajax({ url: "/api/entries/accept/" + ($('#entryToggle').is(":checked") ? "1" : "0"), complete: setTimeout(refreshEntryToggle, 1000) });
})
$("#entrytable").bootstrapTable().on('load-success.bs.table', function () {
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
}) })
$('[data-toggle="tooltip"]').tooltip({
trigger: 'hover'
})
}) })
function confirmDeleteEntry(name, entry_id) { function confirmDeleteEntry(name, entry_id) {
bootbox.confirm("Wirklich den Eintrag von " + name + " löschen?", function (result) { bootbox.confirm("Wirklich den Eintrag von "+name+" löschen?", function(result){
if (result) { if (result) {
deleteEntry(entry_id) deleteEntry(entry_id)
} }
@ -59,7 +69,7 @@
} }
function confirmDeleteSelectedEntries() { function confirmDeleteSelectedEntries() {
bootbox.confirm({ bootbox.confirm({
message: "Wirklich gewählte Eintragungen löschen?", message: "Wirklich gewählte Eintragungen löschen?",
buttons: { buttons: {
confirm: { confirm: {
label: 'Ja', label: 'Ja',
@ -70,7 +80,7 @@
className: 'btn btn-secondary' className: 'btn btn-secondary'
} }
}, },
callback: function (result) { callback: function(result){
if (result) { if (result) {
DeleteSelectedEntries(getIdSelections()) DeleteSelectedEntries(getIdSelections())
} }
@ -90,22 +100,22 @@
className: 'btn btn-secondary' className: 'btn btn-secondary'
} }
}, },
callback: function (result) { callback: function(result){
if (result) { if (result) {
var dialog = bootbox.dialog({ var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Aktualisiere Song-Datenbank...</p>', message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Aktualisiere Song-Datenbank...</p>',
closeButton: false closeButton: false
}); });
updateSongDatabase(dialog) updateSongDatabase(dialog)
} }
} }
}) })
} }
function refreshEntryToggle() { function refreshEntryToggle() {
$.getJSON("/api/entries/accept", (data) => { $.getJSON("/api/entries/accept", (data) => {
if (data["value"] != $('#entryToggle').is(":checked")) { if (data["value"]!=$('#entryToggle').is(":checked")) {
if (data["value"] == 1) { if(data["value"]==1) {
$('#entryToggle').data('bs.toggle').on('true') $('#entryToggle').data('bs.toggle').on('true')
} }
else { else {
@ -115,53 +125,53 @@
}) })
} }
function deleteEntry(entry_id) { function deleteEntry(entry_id) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/api/entries/delete/' + entry_id, url: '/api/entries/delete/'+entry_id,
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
async: false async: false
}); });
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
} }
function markEntryAsSung(entry_id) { function markEntryAsSung(entry_id) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/api/entries/mark_sung/' + entry_id, url: '/api/entries/mark_sung/'+entry_id,
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
async: false async: false
}); });
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
} }
function markEntryAsTransferred(entry_id) { function markEntryAsTransferred(entry_id) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/api/entries/mark_transferred/' + entry_id, url: '/api/entries/mark_transferred/'+entry_id,
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
async: false async: false
}); });
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
} }
function DeleteSelectedEntries(ids) { function DeleteSelectedEntries(ids) {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/api/entries/delete', url: '/api/entries/delete',
data: JSON.stringify(ids), // or JSON.stringify ({name: 'jonas'}), data: JSON.stringify(ids), // or JSON.stringify ({name: 'jonas'}),
error: function () { error: function() {
bootbox.alert({ bootbox.alert({
message: "Fehler beim Löschen der Eintragungen.", message: "Fehler beim Löschen der Eintragungen.",
}) })
}, },
success: function () { success: function() {
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
}, },
contentType: "application/json", contentType: "application/json",
dataType: 'json' dataType: 'json'
@ -173,47 +183,29 @@
url: '/api/songs/update', url: '/api/songs/update',
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
success: function (data) { success: function(data) {
wait_dialog.modal('hide') wait_dialog.modal('hide')
bootbox.alert({ bootbox.alert({
message: data["status"], message: data["status"],
callback: function () { callback: function() {
$("#entrytable").bootstrapTable('refresh') $("#entrytable").bootstrapTable('refresh')
} }
}) })
} }
}); });
} }
function TableActions(value, row, index) { function TableActions (value, row, index) {
let outerHTML = "" let outerHTML = ""
if (row.Transferred == 1) { if (row.Transferred==1) {
outerHTML = "<button type=\"button\" class=\"btn btn-default\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Markierung zurückziehen\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');markEntryAsTransferred(" + row.entry_ID + ")\"><i class=\"fas fa-backward\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');markEntryAsSung(" + row.entry_ID + ")\"><i class=\"fas fa-check\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-danger\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');confirmDeleteEntry(this.parentElement.parentElement.children[1].innerText," + row.entry_ID + ")\"><i class=\"fas fa-trash\"></i></button>"; outerHTML = "<button type=\"button\" class=\"btn btn-default\" onclick=\"markEntryAsTransferred("+row.ID+")\"><i class=\"fas fa-backward\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"markEntryAsSung("+row.ID+")\"><i class=\"fas fa-check\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-danger\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick=\"confirmDeleteEntry('"+row.Name+"',"+row.ID+")\"><i class=\"fas fa-trash\"></i></button>";
} else { } else {
outerHTML = "<button type=\"button\" class=\"btn btn-info\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als übertragen markieren\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');markEntryAsTransferred(" + row.entry_ID + ")\"><i class=\"fas fa-exchange-alt\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');markEntryAsSung(" + row.entry_ID + ")\"><i class=\"fas fa-check\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-danger\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick=\"event.stopPropagation();$(this).tooltip('dispose');confirmDeleteEntry(this.parentElement.parentElement.children[1].innerText," + row.entry_ID + ")\"><i class=\"fas fa-trash\"></i></button>"; outerHTML = "<button type=\"button\" class=\"btn btn-info\" onclick=\"markEntryAsTransferred("+row.ID+")\"><i class=\"fas fa-exchange-alt\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-success\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Als gesungen markieren\" onclick=\"markEntryAsSung("+row.ID+")\"><i class=\"fas fa-check\"></i></button>&nbsp;<button type=\"button\" class=\"btn btn-danger\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Eintrag löschen\" onclick=\"confirmDeleteEntry('"+row.Name+"',"+row.ID+")\"><i class=\"fas fa-trash\"></i></button>";
} }
return outerHTML; return outerHTML;
} }
function CopyFormatter(value, row, index) {
let escapedString = value.replace("\"", "\\\"").replace("\'", "\\\'")
return "<span onclick='copyAndNotify(this.innerText)'>" + value + "</span>";
}
function getIdSelections() { function getIdSelections() {
return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) { return $.map($("#entrytable").bootstrapTable('getSelections'), function (row) {
return row.entry_ID return row.ID
})
}
function copyAndNotify(text) {
navigator.clipboard.writeText(text).then(function () {
let toast = {
title: "Kopiert",
message: text,
status: 1,
timeout: 5000
};
Toast.create(toast);
}) })
} }
</script> </script>

View File

@ -2,8 +2,8 @@
{% block title %}Abspielliste{% endblock %} {% block title %}Abspielliste{% endblock %}
{% block content %} {% block content %}
<div id="toolbar"> <div id="toolbar">
<button type="button" class="topbutton btn btn-danger" onclick="confirmCloseEvent()"><i <button type="button" class="topbutton btn btn-danger" onclick="confirmDeleteAllEntries()"><i
class="fas fa-trash mr-2"></i>Event beenden</button> class="fas fa-trash mr-2"></i>Abspielliste löschen</button>
<button type="button" class="topbutton btn btn-primary" onclick="exportPDF()"><i <button type="button" class="topbutton btn btn-primary" onclick="exportPDF()"><i
class="fas fa-file-pdf mr-2"></i>Als PDF herunterladen</button> class="fas fa-file-pdf mr-2"></i>Als PDF herunterladen</button>
<button type="button" class="topbutton btn btn-secondary" onclick="printPDF()"><i <button type="button" class="topbutton btn btn-secondary" onclick="printPDF()"><i
@ -41,17 +41,9 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js"></script>
<script src="https://unpkg.com/jspdf-autotable@3.0.10/dist/jspdf.plugin.autotable.js"></script> <script src="https://unpkg.com/jspdf-autotable@3.0.10/dist/jspdf.plugin.autotable.js"></script>
<script> <script>
function confirmCloseEvent() { function confirmDeleteAllEntries() {
bootbox.confirm({ bootbox.confirm({
message: `Wirklich Ereignis beenden?<br> message: "Wirklich Abspielliste löschen?<br>Stelle sicher, dass du sie vorher zwecks Abrechnung gedruckt und/oder heruntergeladen hast!",
Folgendes wird passieren:<br>
<ul>
<li>Die Warteschlange wird geleert</li>
<li>Die Abspielliste wird gelöscht</li>
<li>Eine neue Event-ID wird vergeben</li>
</ul>
Diese Aktion kann nicht rückgängig gemacht werden!
`,
buttons: { buttons: {
confirm: { confirm: {
label: 'Ja', label: 'Ja',
@ -64,15 +56,15 @@
}, },
callback: function(result){ callback: function(result){
if (result) { if (result) {
closeEvent() deleteAllEntries()
} }
} }
}) })
} }
function closeEvent() { function deleteAllEntries() {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/api/event/close', url: '/api/played/clear',
contentType: "application/json", contentType: "application/json",
dataType: 'json', dataType: 'json',
async: false async: false

View File

@ -4,99 +4,15 @@
<form method="post"> <form method="post">
<p> <p>
<label for="entryquota">Maximale Anzahl an Einträgen pro Nutzer</label> <label for="entryquota">Maximale Anzahl an Einträgen pro Nutzer</label>
<input type="number" class="form-control" id="entryquota" name="entryquota" min=1 <input type="number" class="form-control" id="entryquota" name="entryquota" min=1 value={{app.config['ENTRY_QUOTA']}}>
value={{app.config['ENTRY_QUOTA']}}>
</p> </p>
<p> <p>
<label for="maxqueue">Maximale Anzahl an Einträgen Insgesamt</label> <label for="maxqueue">Maximale Anzahl an Einträgen Insgesamt</label>
<input type="number" class="form-control" id="maxqueue" name="maxqueue" min=1 value={{app.config['MAX_QUEUE']}}> <input type="number" class="form-control" id="maxqueue" name="maxqueue" min=1 value={{app.config['MAX_QUEUE']}}>
</p> </p>
<p>
<label for="theme">Aktives Theme</label>
<select class="form-control" id="theme" name="theme">
{% for theme in themes %}
<option value="{{theme}}" {% if theme==config['THEME'] %}selected{% endif %}>{{theme}}</option>
{% endfor %}
</select>
</p>
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle mr-1"></i>
<strong>Warnung:</strong> Änderungen an den folgenden Einstellungen führen zu einer sofortigen Abmeldung!
</div>
<p>
<label for="username">Benutzername</label>
<input type="text" class="form-control" id="username" name="username"
value={{app.config['BASIC_AUTH_USERNAME']}}>
</p>
<p>
<label for="password">Passwort ändern</label>
<input type="password" class="form-control" id="password" name="password">
</p>
<input type="submit" class="btn btn-primary mr-1 mb-2" value="Einstellungen anwenden"> <input type="submit" class="btn btn-primary mr-1 mb-2" value="Einstellungen anwenden">
<input type="button" class="btn btn-default mr-1 mb-2" onclick="$.get('/writeSettings').done(()=>{alert('Einstellungen gespeichert')}).fail(()=>{alert('Fehler beim Speichern der Einstellungen')})" value="Einstellungen speichern"/>
</form> </form>
<form>
<p>
<label for="statsImport">Statistiken importieren/exportieren</label>
</p>
<div class="row">
<div class="col-sm-4">
<a class="btn btn-secondary" type="button" id="statsExport" href="/api/songs/stats.csv"><i class="fas fa-download mr-1"></i>Exportieren</a>
</div>
<div class="col input-group mb-3">
<div class="custom-file mr-1">
<input type="file" class="custom-file-input" id="statsImport" data-allowed-file-extensions='["csv"]'>
<label class="custom-file-label" for="statsImport">CSV-Datei auswählen</label>
</div>
<button class="btn btn-secondary" type="button" id="statsImportBtn"><i class="fas fa-upload mr-1"></i>Importieren</button>
</div>
</div>
<p>
</p>
</form>
<details>
<summary>Current config:</summary>
<pre>{% for key, val in config.items() %}{{key}}: {{val}}<br>{% endfor %}</pre>
</details>
{% endblock %} {% endblock %}
{% block extrajs %} {% block extrajs %}
<script>
$(document).ready(function () {
$('#statsImport').on('change', function () {
var fileName = $(this).val().split('\\').pop();
$(this).next('.custom-file-label').html(fileName);
});
$('#statsImportBtn').on('click', function () {
var file_data = $('#statsImport').prop('files')[0];
var form_data = new FormData();
form_data.append('file', file_data);
$.ajax({
url: '/api/songs/stats.csv',
cache: false,
contentType: false,
processData: false,
data: form_data,
type: 'post',
success: function (response) {
toast = {
title: "Erfolgreich importiert",
message: "Die Statistiken wurden erfolgreich importiert.",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
},
error: function (response) {
toast = {
title: "Fehler beim Importieren",
message: "Die Statistiken konnten nicht importiert werden.",
status: TOAST_STATUS.ERROR,
timeout: 5000
}
Toast.create(toast);
}
});
});
});
</script>
{% endblock %} {% endblock %}

View File

@ -1,8 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Songsuche{% endblock %} {% block title %}Songsuche{% endblock %}
{% block content %} {% block content %}
<input class="form-control" id="filter" type="text" placeholder="Suche nach einem Song..."> <input class="form-control" id="filter" type="text" placeholder="Suchen...">
<h4 id="suggestionExplainer" class="mt-3 mb-3 text-center" style="display: none;">Oder probiere es mit einem dieser Vorschläge:</h4>
<table class="table"> <table class="table">
<tbody id="songtable"> <tbody id="songtable">
</tbody> </tbody>
@ -12,52 +11,21 @@
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Auf die Liste setzen</h5> <h5 class="modal-title" id="exampleModalLabel">Auf Liste setzen</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="songinfo container">
<div class="row">
<div class="col">
<h5 id="songTitle"></h5>
<p><span id="songArtist"></span>&nbsp;<span id="songYear"></span></p>
</div>
</div>
<div class="row">
<div class="col-1">
<p><i class="fas fa-info"></i></p>
</div>
<div class="col" id="indicators">
</div>
</div>
<div class="row">
<div class="col-1">
<p><i class="fas fa-file-audio"></i></p>
</div>
<div class="col">
<p id="songGenres"></p>
</div>
</div>
<div class="row">
<div class="col-1">
<p><i class="fas fa-language"></i></p>
</div>
<div class="col">
<p id="songLanguages"></p>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<form id="nameForm"> <form id="nameForm">
<div class="modal-body"> <div class="modal-body">
<label for="singerNameInput">Dein Name:</label> <label for="singerNameInput">Sängername</label>
<input type="text" class="form-control" id="singerNameInput" placeholder="Max Mustermann" <input type="text" class="form-control" id="singerNameInput" placeholder="Max Mustermann"
required> required>
<input id="selectedId" name="selectedId" type="hidden" value=""> <input id="selectedId" name="selectedId" type="hidden" value="">
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Schließen</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" id="submitSongButton">Anmelden</button> <button type="submit" class="btn btn-primary" id="submitSongButton">Anmelden</button>
</div> </div>
</form> </form>
@ -69,79 +37,38 @@
{% block extrajs %} {% block extrajs %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
getSuggestions(10); $("#filter").focus();
$("#filter").keyup(debounce(() => songSearch())); $("#filter").keyup(function () {
var value = $(this).val().toLowerCase();
//alert(value);
if (value.length >= 1) {
$.getJSON("/api/songs/compl", { search: value }, function (data) {
var items = [];
$.each(data, function (key, val) {
items.push("<tr><td>" + val[0] + `</td>
<td><button type='button'
class='btn btn-primary justify-content-center align-content-between enqueueButton'
data-toggle='modal'
data-target='#enqueueModal' onclick='setSelectedId(`+ val[1] + `)'><i
class="fas fa-plus"></i></button></td>
</tr>`)
});
$("#songtable").html("")
$(items.join("")).appendTo("#songtable");
entriesAccepted()
});
} else {
$("#songtable").html("")
}
});
$("#nameForm").submit(function (e) { $("#nameForm").submit(function (e) {
e.preventDefault(); e.preventDefault();
submitModal(); submitModal();
}); });
$('#enqueueModal').on('shown.bs.modal', function (e) {
$("#singerNameInput").focus();
})
}); });
function getSuggestions(count) {
$.getJSON("/api/songs/suggest", { count: count }, function (data) {
console.log(data);
if (data.length == 0) {
console.log("No suggestions");
$("#suggestionExplainer").hide();
} else {
$("#suggestionExplainer").show();
}
$.each(data, function (key, val) {
$("#songtable").append(constructResultRow(val))
});
});
}
function constructResultRow(data) {
let itemRow = document.createElement("tr")
let itemCell = document.createElement("td")
itemCell.innerHTML = data["artist"] + ` - ` + data["title"]
itemRow.appendChild(itemCell)
let infoCell = document.createElement("td")
let duoindicator = document.createElement("i")
duoindicator.classList.add("fas")
if (data["duo"] == 0) {
duoindicator.classList.add("fa-user")
}
if (data["duo"] == 1) {
duoindicator.classList.add("fa-user-friends")
}
duoindicator.classList.add("ml-1")
duoindicator.classList.add("list-indicator")
infoCell.appendChild(duoindicator)
if (data["explicit"] == 1) {
let explicitindicator = document.createElement("i")
explicitindicator.classList.add("fas")
explicitindicator.classList.add("fa-e")
explicitindicator.classList.add("ml-1")
infoCell.appendChild(explicitindicator)
}
itemRow.appendChild(infoCell)
let buttonCell = document.createElement("td")
let button = document.createElement("button")
button.classList.add("btn")
button.classList.add("btn-primary")
button.classList.add("justify-content-center")
button.classList.add("align-content-between")
button.classList.add("enqueueButton")
button.setAttribute("type", "button")
button.setAttribute("data-toggle", "modal")
button.setAttribute("data-target", "#enqueueModal")
button.setAttribute("onclick", "setSelectedId(" + data["karafun_id"] + ")")
let buttonIcon = document.createElement("i")
buttonIcon.classList.add("fas")
buttonIcon.classList.add("fa-plus")
button.appendChild(buttonIcon)
buttonCell.appendChild(button)
itemRow.appendChild(buttonCell)
return itemRow
}
function enqueue(client_id, id, name, success_callback, blocked_callback) { function enqueue(client_id, id, name, success_callback, blocked_callback) {
var data = { var data = {
@ -152,7 +79,7 @@
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/api/enqueue', url: '/api/enqueue',
data: JSON.stringify(data), data: JSON.stringify(data), // or JSON.stringify ({name: 'jonas'}),
success: success_callback, success: success_callback,
statusCode: { statusCode: {
423: blocked_callback 423: blocked_callback
@ -163,109 +90,18 @@
} }
function setSelectedId(id) { function setSelectedId(id) {
$("#songArtist").html("");
$("#songTitle").html("");
$("#songYear").html("");
$("#indicators")[0].innerHTML = "";
$("#selectedId").attr("value", id); $("#selectedId").attr("value", id);
$.getJSON("/api/songs/details/" + id, function (data) {
$("#songTitle").html(data["title"]);
$("#songArtist").html(data["artist"]);
$("#songYear").html(data["year"]);
$("#indicators")[0].innerHTML = "";
let duoindicator_badge = document.createElement("span");
duoindicator_badge.classList.add("badge");
duoindicator_badge.classList.add("badge-secondary");
duoindicator_badge.classList.add("badge-pill");
duoindicator_badge.classList.add("mx-1");
duoindicator_badge.classList.add("p-2");
if (data["duo"] == 0) {
duoindicator_badge.innerHTML = "Solo";
let duoindicator = document.createElement("i");
duoindicator.classList.add("fas");
duoindicator.classList.add("fa-user");
duoindicator.classList.add("ml-1");
duoindicator_badge.appendChild(duoindicator);
$("#indicators")[0].appendChild(duoindicator_badge)
}
if (data["duo"] == 1) {
duoindicator_badge.innerHTML = "Duo";
let duoindicator = document.createElement("i");
duoindicator.classList.add("fas");
duoindicator.classList.add("fa-user-friends");
duoindicator.classList.add("ml-1");
duoindicator_badge.appendChild(duoindicator);
$("#indicators")[0].appendChild(duoindicator_badge)
}
if (data["explicit"] == 1) {
let explicitindicator_badge = document.createElement("span");
explicitindicator_badge.classList.add("badge");
explicitindicator_badge.classList.add("badge-secondary");
explicitindicator_badge.classList.add("badge-pill");
explicitindicator_badge.classList.add("mx-1");
explicitindicator_badge.classList.add("p-2");
explicitindicator_badge.innerHTML = "Explicit";
let explicitindicator = document.createElement("i");
explicitindicator.classList.add("fas");
explicitindicator.classList.add("fa-e");
explicitindicator.classList.add("ml-1");
explicitindicator_badge.appendChild(explicitindicator);
$("#indicators")[0].appendChild(explicitindicator_badge)
}
let styles = data["styles"].split(",");
let languages = data["languages"].split(",");
$("#songGenres").html("");
$("#songLanguages").html("");
for (let i = 0; i < styles.length; i++) {
let badge = document.createElement("span");
badge.classList.add("badge");
badge.classList.add("badge-secondary");
badge.classList.add("badge-pill");
badge.classList.add("mx-1");
badge.classList.add("p-2");
badge.innerHTML = styles[i];
$("#songGenres")[0].appendChild(badge);
}
for (let i = 0; i < languages.length; i++) {
let badge = document.createElement("span");
badge.classList.add("badge");
badge.classList.add("badge-secondary");
badge.classList.add("badge-pill");
badge.classList.add("mx-1");
badge.classList.add("p-2");
badge.innerHTML = languages[i];
$("#songLanguages")[0].appendChild(badge);
}
});
} }
function submitModal() { function submitModal() {
var name = $("#singerNameInput").val(); var name = $("#singerNameInput").val();
var id = $("#selectedId").attr("value"); var id = $("#selectedId").attr("value");
enqueue(localStorage.getItem("clientId"), id, name, function (response) { enqueue(localStorage.getItem("clientId"),id, name, function () {
entryID = response["entry_id"];
toast = {
title: "Erfolgreich eingetragen",
message: "Du wurdest erfolgreich eingetragen.",
status: TOAST_STATUS.SUCCESS,
timeout: 5000
}
Toast.create(toast);
addEntry(entryID);
$("#enqueueModal").modal('hide'); $("#enqueueModal").modal('hide');
window.location.href = '/#end'; window.location.href = '/#end';
}, function (response) { }, function (response) {
bootbox.alert({ bootbox.alert({
message: "Deine Eintragung konnte leider nicht vorgenommen werden.\nGrund: " + response.responseJSON.status, message: "Deine Eintragung konnte leider nicht vorgenommen werden.\nGrund: "+response.responseJSON.status,
}); });
entriesAccepted(); entriesAccepted();
$("#enqueueModal").modal('hide'); $("#enqueueModal").modal('hide');
@ -275,36 +111,6 @@
} }
function songSearch() {
let value = $("#filter").val()
if (value.length >= 1) {
$("#suggestionExplainer").hide();
$.getJSON("/api/songs/search", { q: value }, function (data) {
var items = [];
$("#songtable").html("")
$.each(data, function (key, val) {
$("#songtable").append(constructResultRow(val))
});
$(items.join("")).appendTo("#songtable");
entriesAccepted()
});
} else {
$("#songtable").html("")
getSuggestions(10);
}
}
function debounce(func, timeout = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
{% if not auth %} {% if not auth %}
function entriesAccepted() { function entriesAccepted() {
$.getJSON("/api/entries/accept", (data, out) => { $.getJSON("/api/entries/accept", (data, out) => {
@ -312,11 +118,10 @@
$(".enqueueButton").prop("disabled", true) $(".enqueueButton").prop("disabled", true)
$(".enqueueButton").prop("style", "pointer-events: none;") $(".enqueueButton").prop("style", "pointer-events: none;")
$(".enqueueButton").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='top'></span>"); $(".enqueueButton").wrap("<span class='tooltip-span' tabindex='0' data-toggle='tooltip' data-placement='top'></span>");
$(".tooltip-span").prop("title", "Eintragungen sind leider momentan nicht möglich.") $(".tooltip-span").prop("title", "Eintragungen sind leider nicht mehr möglich.")
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
} else { } else {
$(".enqueueButton").prop("disabled", false) $(".enqueueButton").prop("disabled", false)
} }
}) })

View File

@ -1,4 +1,4 @@
from app import app from app import app
if __name__ == "__main__": if __name__ == "__main__":
app.run() app.run()

View File

@ -1,39 +0,0 @@
#!/bin/bash
# Get username from command line
if [ $# -eq 0 ]; then
echo "No username supplied. Please supply a github username as the first argument."
exit 1
fi
# Store username in variable
USERNAME=$1
# Check for uncommitted changes
if ! git diff-index --quiet HEAD --; then
echo "You have uncommitted changes. Please commit or stash them and try again."
exit 1
fi
# Get the appropriate version of the container using git
VERSION=$(git rev-parse --abbrev-ref HEAD)-$(git describe)
# Build the container. Add the version as a tag and as ENV variable SOURCE_VERSION
docker build -t ghcr.io/$USERNAME/karaoqueue:$VERSION --build-arg SOURCE_VERSION=$VERSION .
# Ask the user if they want to push the container. Confirm Version.
read -p "Push container to ghcr.io/$USERNAME/karaoqueue:$VERSION? [y/n] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
docker push ghcr.io/$USERNAME/karaoqueue:$VERSION
fi
# Ask the user if they want to push the container as latest
read -p "Push container to ghcr.io/$USERNAME/karaoqueue:latest? [y/n] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
docker tag ghcr.io/$USERNAME/karaoqueue:$VERSION ghcr.io/$USERNAME/karaoqueue:latest
docker push ghcr.io/$USERNAME/karaoqueue:latest
fi

View File

@ -1,23 +0,0 @@
version: "3.9"
secrets:
secrets:
file: ./secrets.yml
services:
karaoqueue:
image: "ghcr.io/phoenixtwofive/karaoqueue:v2023.06"
build: .
restart: always
ports:
- "127.0.0.1:8081:80" # Please put a reverse proxy in front of this
env_file: .env
db:
image: mariadb
restart: always
env_file: .env
volumes:
- karaoqueue-db:/var/lib/mysql
volumes:
karaoqueue-db:

View File

@ -1,15 +0,0 @@
# This Compose file is for development only. It is not intended for production use.
# It only starts auxiliary services, such as a database, that are required for the
# application to run. The application itself is started separately, using the
# command "python -m flask run" or your favorite IDE.
# Useful for attaching a debugger to the application.
version: "3.9"
services:
db:
image: mariadb
restart: always
env_file: .env.dev
ports:
- "3306:3306"

392
docs/swagger.yaml Normal file
View File

@ -0,0 +1,392 @@
openapi: 3.0.2
info:
title: Karaoqueue API
version: '0.0.1'
servers:
- url: 'http://localhost:3000/api'
description: Local Test sever instance
- url: 'https://karaoke.phillipathome.dynv6.net/api'
description: Production API
paths:
/queue:
get:
summary: 'Fetch entry Queue content'
description: 'Fetch entry Queue'
parameters:
- name: index
in: query
description: Position from which on to return results
required: false
schema:
type: integer
- name: limit
in: query
description: How many items to return at one time (max 100, default 20)
required: false
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/QueueEntry'
'400':
description: Invalid request. Check your parameters.
'404':
description: No Entries found in specified range.
'5XX':
description: Unexpected error.
post:
description: 'Add entry to Queue'
summary: 'Add entry to Queue'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
singer_name:
type: string
song_id:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
entry_id:
type: string
format: bson.ObjectID
pattern: '/^[a-f\d]{24}$/i'
entry_auth:
type: string
'400':
description: Malformed request.
'405':
description: Currently not accepting entries.
delete:
summary: 'Clear queue'
security:
- cookieAuth: []
responses:
'200':
description: OK. Successfully cleared Queue
'401':
description: Not Authorized
description: clear queue
/queue/{entry_id}:
get:
summary: GET single queue entry
parameters:
- in: path
name: entry_id
schema:
type: integer
required: true
description: ID of the Entry to get
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/QueueEntry'
patch:
summary: Change entry
security:
- cookieAuth: []
parameters:
- in: path
name: entry_id
schema:
type: string
format: bson.ObjectID
pattern: '/^[a-f\d]{24}$/i'
required: true
description: >
ID of the entry to modify. One of the following is needed:
- Proper Bearer-Token authorization
- The entry_auth string corresponding to the entry
requestBody:
required: false
content:
application/json:
schema:
type: object
required:
- entry_auth
properties:
singer_name:
type: string
song_id:
type: integer
entry_auth:
type: string
responses:
'200':
description: OK
'404':
description: Entry not found
'405':
description: Method not allowed. Check your entry_auth or authorization.
delete:
summary: 'Delete entry'
security:
- cookieAuth: []
parameters:
- in: path
name: entry_id
schema:
type: string
format: bson.ObjectID
pattern: '/^[a-f\d]{24}$/i'
required: true
description: >
ID of the entry to modify. One of the following is needed:
- Proper Bearer-Token authorization
- The entry_auth string corresponding to the entry
requestBody:
required: false
content:
application/json:
schema:
type: object
required:
- entry_auth
properties:
entry_auth:
type: string
responses:
'200':
description: OK
'404':
description: Entry not found
'405':
description: Method not allowed. Check your entry_auth or authorization.
/songs:
get:
summary: Search in Songs
parameters:
- in: query
name: query
schema:
type: string
required: true
- name: limit
in: query
description: How many items to return at one time (max 100, default 20)
required: false
schema:
type: integer
responses:
'200':
description: OK. An array of Songs according to the Query string.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/SongEntry'
'400':
description: Malformed Request
put:
summary: Update Song Database
description: >
Trigger an update of the database using the source CSV defined
in the config.
security:
- cookieAuth: []
responses:
'200':
description: OK. Songs have been updated
'401':
description: Authorization required. Check your auth.
/statistics:
get:
summary: Statistics about the Database
responses:
'200':
description: Statistics as JSON
content:
application/json:
schema:
type: object
properties:
num_songs:
type: integer
num_entries:
type: integer
/auth/login:
post:
summary: Logs in and returns the authentication cookie
requestBody:
required: true
description: A JSON object containing the login and password.
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
security: [] # no authentication
responses:
'200':
description: >
Successfully authenticated.
The session ID is returned in a cookie named `jwt`. You need to include this cookie in subsequent requests.
headers:
Set-Cookie:
schema:
type: string
example: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiJhZG1pbiIsImlhdCI6MTYwMTY1MDYwNSwiZXhwIjoxNjAxNjU0MjA1fQ.uGvOlBAZdbPT8U9s7jEt5PUWyxLrpgaf02EoPVC_Zlsd; Path=/; HttpOnly
/auth/logout:
get:
summary: Logs the user out and invalidates the session on the server
security:
- cookieAuth: []
responses:
'200':
description: OK.
'401':
description: Authorization required.
/rpc/end_event:
get:
summary: End the current event
description: Locks entries and does not allow reopening without reset
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/start_event:
get:
summary: Start new event. Clears entries and stats.
description: Sets up a clean state.
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/enable_registration:
get:
summary: Enables registration in the queue
description: Makes it possible for guests to register in the queue.
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/disable_registration:
get:
summary: Disables registration in the queue
description: Makes it impossible for guests to register in the queue.
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/get_playstats:
get:
summary: Get stats of played songs in the current event.
description: Returns the stats for the current evening as JSON
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/download_playstats:
get:
summary: Get stats of played songs in the current event for download.
description: Returns the stats for the current evening as PDF (for example for GEMA)
security:
- cookieAuth: []
responses:
'200':
description: OK.
/rpc/entry_fulfilled:
get:
summary: Mark an entry as fulfilled.
description: Mark an entry as fulfilled. This adds it to the statistics.
security:
- cookieAuth: []
responses:
'200':
description: OK.
parameters:
- in: query
name: entry_id
schema:
type: string
description: The id of the entry to mark as done.
components:
securitySchemes:
cookieAuth: # arbitrary name for the security scheme; will be used in the "security" key later
type: apiKey
in: cookie
name: jwt # cookie name
schemas:
QueueEntry:
type: object
properties:
_id:
type: string
format: bson.ObjectID
pattern: '/^[a-f\d]{24}$/i'
singer_name:
type: string
song_id:
type: integer
SongEntry:
type: object
properties:
id:
type: integer
title:
type: string
artist:
type: string
year:
type: integer
duet:
type: boolean
explicit:
type: boolean
styles:
type: array
items:
type: string
languages:
type: array
items:
type: string
LoginRequest:
type: object
properties:
username:
type: string
password:
type: string
format: password

View File

@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "firefox",
"request": "launch",
"reAttach": true,
"name": "Launch Firefox against localhost",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}"
},
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}",
"runtimeExecutable": "/usr/bin/chromium"
}
]
}

View File

@ -0,0 +1,26 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"group": "build",
"problemMatcher": []
},
{
"type": "npm",
"script": "build",
"group": "build",
"problemMatcher": []
},
{
"type": "npm",
"script": "serve",
"problemMatcher": [
"$tsc"
]
}
]
}

View File

@ -0,0 +1,135 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ng-karaoqueue": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/ng-karaoqueue",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": false,
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "mdi.svg",
"input": "node_modules/@mdi/angular-material/",
"output": "/assets/"
}
],
"styles": [
"src/custom-theme.scss",
"src/styles.scss"
],
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "ng-karaoqueue:build"
},
"configurations": {
"production": {
"browserTarget": "ng-karaoqueue:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "ng-karaoqueue:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "mdi.svg",
"input": "node_modules/@mdi/angular-material/",
"output": "/assets/"
}
],
"styles": [
"./app/custom-theme.scss",
"src/styles.scss"
],
"scripts": []
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "ng-karaoqueue:serve"
},
"configurations": {
"production": {
"devServerTarget": "ng-karaoqueue:serve:production"
}
}
}
}
}
},
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
}
}

11965
frontend/ng-karaoqueue/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,51 @@
{
"name": "ng-karaoqueue",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"serve": "ng serve --host=0.0.0.0",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^14.2.3",
"@angular/cdk": "^14.2.2",
"@angular/common": "^14.2.3",
"@angular/compiler": "^14.2.3",
"@angular/core": "^14.2.3",
"@angular/forms": "^14.2.3",
"@angular/material": "^14.2.2",
"@angular/platform-browser": "^14.2.3",
"@angular/platform-browser-dynamic": "^14.2.3",
"@angular/router": "^14.2.3",
"@mdi/angular-material": "^7.0.96",
"hammerjs": "^2.0.8",
"rxjs": "~7.5.6",
"tslib": "^2.4.0",
"zone.js": "~0.11.8"
},
"devDependencies": {
"@angular/cli": "^14.2.3",
"@angular/compiler-cli": "^14.2.3",
"@angular/language-service": "^14.2.3",
"@types/jasmine": "~4.3.0",
"@types/jasminewd2": "~2.0.10",
"@types/node": "~18.7.19",
"codelyzer": "^0.0.28",
"jasmine-core": "~4.4.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.1",
"karma-chrome-launcher": "~3.1.1",
"karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",
"protractor": "~7.0.0",
"ts-node": "~10.9.1",
"tslint": "~6.1.0",
"typescript": "~4.8.3"
}
}

View File

@ -0,0 +1,16 @@
import { EntryListingComponent } from './entry-listing/entry-listing.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SongSearchComponent } from './song-search/song-search.component';
const routes: Routes = [
{path: '', component: EntryListingComponent },
{path: 'songs', component: SongSearchComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1,11 @@
<div>
<mat-toolbar color="primary" class="mat-elevation-z6">
<span>KaraoQueue</span>
<span>&nbsp;</span>
<a mat-button routerLink="/" [routerLinkActive]="['is-active']">Warteschlange</a>
<a mat-button routerLink="/songs" [routerLinkActive]="['is-active']">Songs</a>
<div class="spacer"></div>
<a mat-stroked-button>Login</a>
</mat-toolbar>
</div>
<router-outlet></router-outlet>

View File

@ -0,0 +1,7 @@
.spacer {
flex: 1 1 auto;
}
.is-active {
background-color: rgba(255,255,255,0.1);
}

View File

@ -0,0 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { RuntimeConfigLoaderService } from 'runtime-config-loader';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit{
constructor(private configSvc: RuntimeConfigLoaderService) {}
ngOnInit(): void {
console.log("API at ",this.configSvc.getConfigObjectKey("api"));
}
title = 'KaraoQueue';
}

View File

@ -0,0 +1,107 @@
import { BrowserModule, DomSanitizer } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatBadgeModule } from '@angular/material/badge';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatStepperModule } from '@angular/material/stepper';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconRegistry, MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSliderModule } from '@angular/material/slider';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree';
import { HttpClientModule } from '@angular/common/http';
import { EntryListingComponent } from './entry-listing/entry-listing.component';
import { SongSearchComponent } from './song-search/song-search.component';
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { RuntimeConfigLoaderModule } from 'runtime-config-loader';
@NgModule({
declarations: [
AppComponent,
EntryListingComponent,
SongSearchComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatStepperModule,
MatDatepickerModule,
MatDialogModule,
MatDividerModule,
MatExpansionModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatTreeModule,
HttpClientModule,
RuntimeConfigLoaderModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(matIconRegistry: MatIconRegistry, domSanitizer: DomSanitizer) {
matIconRegistry.addSvgIconSet(domSanitizer.bypassSecurityTrustResourceUrl('./assets/mdi.svg'));
}
}

View File

@ -0,0 +1 @@
<p>entry-listing works!</p>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EntryListingComponent } from './entry-listing.component';
describe('EntryListingComponent', () => {
let component: EntryListingComponent;
let fixture: ComponentFixture<EntryListingComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ EntryListingComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EntryListingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-entry-listing',
templateUrl: './entry-listing.component.html',
styleUrls: ['./entry-listing.component.scss']
})
export class EntryListingComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { EntryServiceService } from './entry-service.service';
describe('EntryServiceService', () => {
let service: EntryServiceService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(EntryServiceService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { RuntimeConfigLoaderService } from 'runtime-config-loader';
import { Entry } from './models/entry.model';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class EntryServiceService {
private api: string;
constructor(
private http: HttpClient,
private configSvc: RuntimeConfigLoaderService
) {
this.api = configSvc.getConfigObjectKey("api");
}
getEntries(): Observable<Array<Entry>> {
return null; // TODO
}
}

View File

@ -0,0 +1,10 @@
export class Artist {
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
id: number;
name: string;
}

View File

@ -0,0 +1,12 @@
export class Entry {
constructor(singer_name: string, song: string, auth_cookie?: string) {
this.singer_name=singer_name;
this.song=song;
this.auth_cookie = auth_cookie;
}
singer_name: string;
song: string; //Actually the ID of the Song
auth_cookie?: string; //The "cookie" to authenticate for changing an entry. Null otherwise
}

View File

@ -0,0 +1,16 @@
export class Genre {
constructor(id: number, name: string) {
this.id = id;
this._name = name;
}
public get name() : string {
return this._name;
}
id: number;
_name: string;
}

View File

@ -0,0 +1,10 @@
export class Language {
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
id: number;
name: string;
}

View File

@ -0,0 +1,26 @@
import { Genre } from './genre.model';
import { Language } from './language.model';
import { Artist } from './artist.model';
export class Song {
constructor(title: string, artist: Artist, karafun_id: number, duet: boolean, explicit: boolean, id: number, genres: Array<String>, languages: Array<String>) {
this.title=title;
this.artist=artist;
this.karafun_id=karafun_id;
this.duet=duet;
this.explicit=explicit;
this.id=id;
this.genres=genres;
this.languages=languages;
}
title: string;
artist: Artist;
karafun_id: number;
duet: boolean;
explicit: boolean;
id: number;
genres: Array<String>;
languages: Array<String>;
}

View File

@ -0,0 +1,48 @@
<div class="content">
<mat-form-field class="search-input">
<mat-label>Suche...</mat-label>
<mat-icon matSuffix svgIcon="magnify"></mat-icon>
<input type="text" matInput placeholder="Tippe einen Künstler oder Song..."
(keyup)="applyFilter($event.target.value)">
</mat-form-field>
</div>
<div class="resultcontainer">
<mat-list *ngFor="let song of songs">
<mat-card class="result">
<div class="card-left">
<mat-card-title>{{song.artist}} - {{song.title}}</mat-card-title>
<div class="song-info">
<div class="info-icons">
<ng-template [ngIf]="song.duet==true" [ngIfElse]="nonduet">
<mat-icon svgIcon="account-multiple"></mat-icon>
</ng-template>
<ng-template #nonduet>
<mat-icon svgIcon="account"></mat-icon>
</ng-template>
<ng-template [ngIf]="song.explicit==true" [ngIfElse]="nonexplicit">
<mat-icon svgIcon="alpha-e-box"></mat-icon>
</ng-template>
<ng-template #nonexplicit>
<mat-icon svgIcon="alpha-e-box" class="icon-disabled"></mat-icon>
</ng-template>
</div>
<div class="genre-list">
<mat-icon svgIcon="music-circle"></mat-icon>
<mat-chip-list *ngFor="let genre of song.genres">
<mat-chip>{{genre}}</mat-chip>
</mat-chip-list>
</div>
<div class="language-list">
<mat-icon svgIcon="account-voice"></mat-icon>
<mat-chip-list *ngFor="let language of song.languages">
<mat-chip>{{language}}</mat-chip>
</mat-chip-list>
</div>
</div>
</div>
<button mat-flat-button color="accent" class="add-button">
<mat-icon svgIcon="playlist-plus"></mat-icon>
</button>
</mat-card>
</mat-list>
</div>

View File

@ -0,0 +1,120 @@
.search-input {
width: 95%;
margin-top: 1rem;
}
.add-button {
height: 3rem;
}
.content {
overflow: hidden;
display: flex;
justify-content: center;
}
.resultcontainer {
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
}
.result {
margin-left: auto;
margin-right: auto;
width: 90%;
margin-top: 0.2rem;
display: flex;
align-items: stretch;
justify-content: space-between;
flex-direction: column;
}
.card-left {
display: flex;
flex-direction: column;
width: 100%;
}
.song-info {
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
}
.song-info > * {
margin-right: 0.5rem;
}
.icon-disabled {
opacity: 0.1;
}
.genre-list > mat-icon {
padding-right: 0.25rem;
}
.genre-list mat-chip-list {
margin-right: 0.15rem;
}
.genre-list {
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
display: flex;
flex-direction: row;
padding-bottom: 0.25rem;
align-items: center;
-webkit-mask-image: linear-gradient(to right, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 2px,rgba(0,0,0,1) calc(100% - 2px),rgba(0,0,0,0) 100%);
mask-image: linear-gradient(to right, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 2px,rgba(0,0,0,1) calc(100% - 2px),rgba(0,0,0,0) 100%);
-ms-overflow-style: none;
}
.language-list > mat-icon {
padding-right: 0.25rem;
}
.language-list mat-chip-list {
margin-right: 0.15rem;
}
.genre-list::-webkit-scrollbar {
width: 0px;
height: 0px;
display: none;
}
.language-list {
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
display: flex;
flex-direction: row;
padding-bottom: 0.25rem;
align-items: center;
-ms-overflow-style: none;
}
.language-list::-webkit-scrollbar {
display: none;
}
$breakpoint-tablet: 768px;
@media (min-width: $breakpoint-tablet) {
.result {
flex-direction: row;
}
.add-button {
height: 6.5rem;
width: 4rem;
align-self: center;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SongSearchComponent } from './song-search.component';
describe('SongSearchComponent', () => {
let component: SongSearchComponent;
let fixture: ComponentFixture<SongSearchComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SongSearchComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SongSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,43 @@
import { Component, OnInit } from '@angular/core';
import { SongServiceService } from "../song-service.service";
import { Song } from '../models/song.model';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
@Component({
selector: 'app-song-search',
templateUrl: './song-search.component.html',
styleUrls: ['./song-search.component.scss']
})
export class SongSearchComponent implements OnInit {
private searchSub$ = new Subject<string>();
constructor(private songServiceService: SongServiceService) { }
songs: Array<Song> = new Array<Song>();
updateSongs(text: string) {
this.songServiceService.searchSongByText(text).subscribe(x => {
console.log(x);
this.songs = x;
});
}
applyFilter(filterValue: string) {
this.searchSub$.next(filterValue)
}
ngOnInit(): void {
this.searchSub$.pipe(
debounceTime(400),
distinctUntilChanged()
).subscribe((filterValue: string) => {
this.updateSongs(filterValue);
});
//Für Testzwecke
this.updateSongs("Test");
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SongServiceService } from './song-service.service';
describe('SongServiceService', () => {
let service: SongServiceService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SongServiceService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,42 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Song } from './models/song.model';
import { Artist } from './models/artist.model';
import { Genre } from './models/genre.model';
import { Language } from './models/language.model';
import { RuntimeConfigLoaderService } from 'runtime-config-loader';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class SongServiceService {
private api: string;
constructor(
private http: HttpClient,
private configSvc: RuntimeConfigLoaderService
) {
this.api=configSvc.getConfigObjectKey("api");
}
searchSongByText(text: string): Observable<Array<Song>> {
let out = new Array<Song>();
this.http.get(this.api +"/songs/compl?search="+text).subscribe((data: Observable<JSON>) => {
data.forEach(element => {
out.push(new Song(element["title"],element["artist"],element["karafun_id"],element["duo"],element["explicit"],element["_id"],element["styles"],element["languages"]));
});
});
const observable = new Observable<Array<Song>>( subscriber => {
subscriber.next(out);
})
return observable;
}
}

View File

@ -0,0 +1,3 @@
{
"api":"http://localhost:5000/api"
}

View File

@ -0,0 +1,280 @@
/**
* Generated theme by Material Theme Generator
* https://materialtheme.arcsine.dev
*/
@import '@angular/material/theming';
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Fonts
@import 'https://fonts.googleapis.com/css?family=Material+Icons';
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500');
$fontConfig: (
display-4: mat-typography-level(112px, 112px, 300, 'Roboto', -0.0134em),
display-3: mat-typography-level(56px, 56px, 400, 'Roboto', -0.0089em),
display-2: mat-typography-level(45px, 48px, 400, 'Roboto', 0.0000em),
display-1: mat-typography-level(34px, 40px, 400, 'Roboto', 0.0074em),
headline: mat-typography-level(24px, 32px, 400, 'Roboto', 0.0000em),
title: mat-typography-level(20px, 32px, 500, 'Roboto', 0.0075em),
subheading-2: mat-typography-level(16px, 28px, 400, 'Roboto', 0.0094em),
subheading-1: mat-typography-level(15px, 24px, 500, 'Roboto', 0.0067em),
body-2: mat-typography-level(14px, 24px, 500, 'Roboto', 0.0179em),
body-1: mat-typography-level(14px, 20px, 400, 'Roboto', 0.0179em),
button: mat-typography-level(14px, 14px, 500, 'Roboto', 0.0893em),
caption: mat-typography-level(12px, 20px, 400, 'Roboto', 0.0333em),
input: mat-typography-level(inherit, 1.125, 400, 'Roboto', 1.5px)
);
// Foreground Elements
// Light Theme Text
$dark-text: #000000;
$dark-primary-text: rgba($dark-text, 0.87);
$dark-accent-text: rgba($dark-primary-text, 0.54);
$dark-disabled-text: rgba($dark-primary-text, 0.38);
$dark-dividers: rgba($dark-primary-text, 0.12);
$dark-focused: rgba($dark-primary-text, 0.12);
$mat-light-theme-foreground: (
base: black,
divider: $dark-dividers,
dividers: $dark-dividers,
disabled: $dark-disabled-text,
disabled-button: rgba($dark-text, 0.26),
disabled-text: $dark-disabled-text,
elevation: black,
secondary-text: $dark-accent-text,
hint-text: $dark-disabled-text,
accent-text: $dark-accent-text,
icon: $dark-accent-text,
icons: $dark-accent-text,
text: $dark-primary-text,
slider-min: $dark-primary-text,
slider-off: rgba($dark-text, 0.26),
slider-off-active: $dark-disabled-text,
);
// Dark Theme text
$light-text: #ffffff;
$light-primary-text: $light-text;
$light-accent-text: rgba($light-primary-text, 0.7);
$light-disabled-text: rgba($light-primary-text, 0.5);
$light-dividers: rgba($light-primary-text, 0.12);
$light-focused: rgba($light-primary-text, 0.12);
$mat-dark-theme-foreground: (
base: $light-text,
divider: $light-dividers,
dividers: $light-dividers,
disabled: $light-disabled-text,
disabled-button: rgba($light-text, 0.3),
disabled-text: $light-disabled-text,
elevation: black,
hint-text: $light-disabled-text,
secondary-text: $light-accent-text,
accent-text: $light-accent-text,
icon: $light-text,
icons: $light-text,
text: $light-text,
slider-min: $light-text,
slider-off: rgba($light-text, 0.3),
slider-off-active: rgba($light-text, 0.3),
);
// Background config
// Light bg
$light-background: #fafafa;
$light-bg-darker-5: darken($light-background, 5%);
$light-bg-darker-10: darken($light-background, 10%);
$light-bg-darker-20: darken($light-background, 20%);
$light-bg-darker-30: darken($light-background, 30%);
$light-bg-lighter-5: lighten($light-background, 5%);
$dark-bg-tooltip: lighten(#2c2c2c, 20%);
$dark-bg-alpha-4: rgba(#2c2c2c, 0.04);
$dark-bg-alpha-12: rgba(#2c2c2c, 0.12);
$mat-light-theme-background: (
background: $light-background,
status-bar: $light-bg-darker-20,
app-bar: $light-bg-darker-5,
hover: $dark-bg-alpha-4,
card: $light-bg-lighter-5,
dialog: $light-bg-lighter-5,
tooltip: $dark-bg-tooltip,
disabled-button: $dark-bg-alpha-12,
raised-button: $light-bg-lighter-5,
focused-button: $dark-focused,
selected-button: $light-bg-darker-20,
selected-disabled-button: $light-bg-darker-30,
disabled-button-toggle: $light-bg-darker-10,
unselected-chip: $light-bg-darker-10,
disabled-list-option: $light-bg-darker-10,
);
// Dark bg
$dark-background: #2c2c2c;
$dark-bg-lighter-5: lighten($dark-background, 5%);
$dark-bg-lighter-10: lighten($dark-background, 10%);
$dark-bg-lighter-20: lighten($dark-background, 20%);
$dark-bg-lighter-30: lighten($dark-background, 30%);
$light-bg-alpha-4: rgba(#fafafa, 0.04);
$light-bg-alpha-12: rgba(#fafafa, 0.12);
// Background palette for dark themes.
$mat-dark-theme-background: (
background: $dark-background,
status-bar: $dark-bg-lighter-20,
app-bar: $dark-bg-lighter-5,
hover: $light-bg-alpha-4,
card: $dark-bg-lighter-5,
dialog: $dark-bg-lighter-5,
tooltip: $dark-bg-lighter-20,
disabled-button: $light-bg-alpha-12,
raised-button: $dark-bg-lighter-5,
focused-button: $light-focused,
selected-button: $dark-bg-lighter-20,
selected-disabled-button: $dark-bg-lighter-30,
disabled-button-toggle: $dark-bg-lighter-10,
unselected-chip: $dark-bg-lighter-20,
disabled-list-option: $dark-bg-lighter-10,
);
// Compute font config
@include mat-core($fontConfig);
// Theme Config
body {
--primary-color: #090a28;
--primary-lighter-color: #b5b6bf;
--primary-darker-color: #050518;
--text-primary-color: #{$light-primary-text};
--text-primary-lighter-color: #{$dark-primary-text};
--text-primary-darker-color: #{$light-primary-text};
}
$mat-primary: (
main: #090a28,
lighter: #b5b6bf,
darker: #050518,
200: #090a28, // For slide toggle,
contrast : (
main: $light-primary-text,
lighter: $dark-primary-text,
darker: $light-primary-text,
)
);
$theme-primary: mat-palette($mat-primary, main, lighter, darker);
body {
--accent-color: #b70000;
--accent-lighter-color: #e9b3b3;
--accent-darker-color: #9f0000;
--text-accent-color: #{$light-primary-text};
--text-accent-lighter-color: #{$dark-primary-text};
--text-accent-darker-color: #{$light-primary-text};
}
$mat-accent: (
main: #b70000,
lighter: #e9b3b3,
darker: #9f0000,
200: #b70000, // For slide toggle,
contrast : (
main: $light-primary-text,
lighter: $dark-primary-text,
darker: $light-primary-text,
)
);
$theme-accent: mat-palette($mat-accent, main, lighter, darker);
body {
--warn-color: #ffc600;
--warn-lighter-color: #ffeeb3;
--warn-darker-color: #ffb100;
--text-warn-color: #{$dark-primary-text};
--text-warn-lighter-color: #{$dark-primary-text};
--text-warn-darker-color: #{$dark-primary-text};
}
$mat-warn: (
main: #ffc600,
lighter: #ffeeb3,
darker: #ffb100,
200: #ffc600, // For slide toggle,
contrast : (
main: $dark-primary-text,
lighter: $dark-primary-text,
darker: $dark-primary-text,
)
);
$theme-warn: mat-palette($mat-warn, main, lighter, darker);;
$theme: mat-dark-theme($theme-primary, $theme-accent, $theme-warn);
$altTheme: mat-light-theme($theme-primary, $theme-accent, $theme-warn);
// Theme Init
@include angular-material-theme($theme);
.theme-alternate {
@include angular-material-theme($altTheme);
}
// Specific component overrides, pieces that are not in line with the general theming
// Handle buttons appropriately, with respect to line-height
.mat-raised-button, .mat-stroked-button, .mat-flat-button {
padding: 0 1.15em;
margin: 0 .65em;
min-width: 3em;
line-height: 36.4px
}
.mat-standard-chip {
padding: .5em .85em;
min-height: 2.5em;
}
.material-icons {
font-size: 24px;
font-family: 'Material Icons', 'Material Icons';
.mat-badge-content {
font-family: 'Roboto';
}
}
mat-form-field.mat-form-field {
&.mat-focused {
.mat-form-field-label {
color: $light-text;
}
.mat-form-field-ripple {
background-color: $light-text !important;
}
}
.mat-form-field-label.mat-focused,
.mat-form-field.mat-focused.matform-field-should-float {
color: $light-text;
}
.mat-form-field-underline, .mat-form-field-label {
color: $light-accent-text;
}
.mat-form-field-underline.mat-focused {
background-color: $light-text;
}
}
button.mat-menu-item {
line-height: 24px !important;
}
a.mat-menu-item > mat-icon {
margin-bottom: 14px;
}
.mat-icon svg {
height: 24px;
width: 24px;
}

View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>NgKaraoqueue</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography mat-app-background">
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1,10 @@
/* You can add global styles to this file, and also import other style files */
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}

View File

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/app/app.module.ts",
],
"include": [
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,32 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2020",
"module": "es2020",
"lib": [
"es2020",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}