GHP publish
This commit is contained in:
15
playmaker/.gitignore
vendored
Normal file
15
playmaker/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
*.pyc
|
||||
**/__pycache__/
|
||||
*.swp
|
||||
*.apk
|
||||
.tern-project
|
||||
.ycm_extra_conf.py
|
||||
|
||||
dist/
|
||||
build/
|
||||
node_modules/
|
||||
.ropeproject/
|
||||
eggs/
|
||||
.eggs/
|
||||
*.egg-info/
|
||||
*.egg
|
67
playmaker/Dockerfile
Normal file
67
playmaker/Dockerfile
Normal file
@ -0,0 +1,67 @@
|
||||
FROM python:3-buster
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y git \
|
||||
lib32stdc++6 \
|
||||
lib32gcc1 \
|
||||
lib32z1 \
|
||||
lib32ncurses6 \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
libjpeg-dev \
|
||||
libxml2-dev \
|
||||
libxslt1-dev \
|
||||
openjdk-11-jdk-headless \
|
||||
virtualenv \
|
||||
wget \
|
||||
unzip \
|
||||
zlib1g-dev \
|
||||
less \
|
||||
mc \
|
||||
nano
|
||||
|
||||
RUN mkdir -p /data/fdroid/repo && \
|
||||
mkdir -p /opt/playmaker
|
||||
|
||||
RUN wget https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip \
|
||||
&& echo "F10F9D5BCA53CC27E2D210BE2CBC7C0F1EE906AD9B868748D74D62E10F2C8275 commandlinetools-linux-6200805_latest.zip" | sha256sum -c \
|
||||
&& unzip commandlinetools-linux-6200805_latest.zip \
|
||||
&& rm commandlinetools-linux-6200805_latest.zip
|
||||
|
||||
RUN mkdir /opt/android-sdk-linux \
|
||||
&& mv tools /opt/android-sdk-linux/tools
|
||||
|
||||
ENV ANDROID_HOME=/opt/android-sdk-linux
|
||||
ENV PATH=$PATH:$ANDROID_HOME/tools
|
||||
|
||||
RUN echo 'y' | /opt/android-sdk-linux/tools/bin/sdkmanager --sdk_root=/opt/android-sdk-linux --verbose --install "platforms;android-28" "build-tools;28.0.3"
|
||||
|
||||
RUN echo 'y' | rm -rf tools
|
||||
|
||||
COPY README.md setup.py pm-server /opt/playmaker/
|
||||
COPY playmaker /opt/playmaker/playmaker
|
||||
|
||||
WORKDIR /opt/playmaker
|
||||
|
||||
RUN pip3 install fdroidserver
|
||||
|
||||
RUN pip3 install .
|
||||
|
||||
RUN rm -rf /opt/playmaker
|
||||
|
||||
RUN groupadd -g 999 pmuser && \
|
||||
useradd -m -u 999 -g pmuser pmuser
|
||||
|
||||
RUN chown -R pmuser:pmuser /data/fdroid && \
|
||||
chown -R pmuser:pmuser /opt/playmaker
|
||||
|
||||
RUN mkdir -p /usr/local/share/doc/fdroidserver/examples && \
|
||||
cp -r /usr/local/lib/python3.9/site-packages/usr/local/share/doc/fdroidserver/examples/* /usr/local/share/doc/fdroidserver/examples
|
||||
|
||||
USER pmuser
|
||||
|
||||
VOLUME /data/fdroid
|
||||
WORKDIR /data/fdroid
|
||||
|
||||
EXPOSE 5000
|
||||
ENTRYPOINT python3 -u /usr/local/bin/pm-server --fdroid --debug
|
13
playmaker/Makefile
Normal file
13
playmaker/Makefile
Normal file
@ -0,0 +1,13 @@
|
||||
VERSION=0.6.4
|
||||
IMAGE=playmaker
|
||||
REGISTRY=registry.0xace.cc
|
||||
|
||||
.PHONY: build push all
|
||||
|
||||
build:
|
||||
docker build -t $(REGISTRY)/$(IMAGE):$(VERSION) .
|
||||
|
||||
push:
|
||||
docker push $(REGISTRY)/$(IMAGE):$(VERSION)
|
||||
|
||||
all: build push
|
93
playmaker/README.md
Normal file
93
playmaker/README.md
Normal file
@ -0,0 +1,93 @@
|
||||
# Playmaker
|
||||
|
||||

|
||||
|
||||
## Description & Features
|
||||
|
||||
Playmaker is a fdroid repository manager, which lets you download/update apps from the play store using your google account
|
||||
and configure repository with app you download. After you setup the server, repository will be available at the address
|
||||
`http[s]://<playmaker_host>/fdroid`, and you can start downloading apps from play store.
|
||||
|
||||
Server uses [googleplay-api](https://github.com/NoMore201/googleplay-api) library, which is the python equivalent of the Java [play-store-api](https://github.com/yeriomin/play-store-api) library used by YalpStore.
|
||||
|
||||
Features:
|
||||
* Download apks from google play store to your collection
|
||||
* Generate a fdroid repository serving apks downloaded, directly from `<pm_url>/fdroid`
|
||||
* Configure automatic updates of app+repo through a Crontab string
|
||||
* Non-blocking UI, you can browse the collection or search for an app while the server is updating the fdroid
|
||||
repository.
|
||||
* Responsive UI, usable also from a mobile device
|
||||
|
||||
## Configuration
|
||||
|
||||
### Authentication
|
||||
|
||||
To avoid authentication problems, like captcha requests, it's recommended to setup app specific password, and securing your account with 2-factor auth. There are two ways to login to Play Store:
|
||||
|
||||
- Providing credentials in a configuration file
|
||||
- Through a login page.
|
||||
|
||||
The default behaviour is to ask credentials with a login page, when accessing playmaker on first launch. In order to skip login page, it is possible to provide google credentials through a configuration file. Just put `credentials.conf` inside the playmaker directory, with this structure:
|
||||
|
||||
```
|
||||
[google]
|
||||
email = myemail@gmail.com
|
||||
password = mypassword
|
||||
```
|
||||
|
||||
To restrict access to that file, ensure it is readable only by user running playmaker.
|
||||
|
||||
### HTTPS
|
||||
|
||||
It's recommended to configure playmaker with HTTPS, especially with the login page authentication, since playmaker needs to send to the server credentials in plaintext. You can setup it in conjunction with a proxy like nginx, or provide certificate directly to playmaker.
|
||||
|
||||
## Running
|
||||
|
||||
Since this app requires a lot of heavy dependencies, like Android SDK and fdroidserver, it is recommended to use the docker image.
|
||||
You can use a pre-built image on [docker hub](https://hub.docker.com/r/nomore201/playmaker/builds/) or build by yourself using provided `Dockerfile`.
|
||||
There are some environment variables you'll want to use:
|
||||
|
||||
- `HTTPS_CERTFILE`: path of the https certificate file
|
||||
- `HTTPS_KEYFILE`: path of the https key file
|
||||
- `LANG_LOCALE`: set a specific locale. Defaults to the system one if not set
|
||||
- `LANG_TIMEZONE`: set a specific timezone. Defaults to `Europe/Berlin` if not set
|
||||
- `CRONTAB_STRING`: crontab string to configure automatic updates. Defaults to every night at 2AM (`0 2 * * *`)
|
||||
- `DEVICE_CODE`: specify a device to be used by playmaker, defaults to `bacon` (OnePlus One) if not specified. For
|
||||
a list of supported devices see [this file](https://raw.githubusercontent.com/NoMore201/googleplay-api/master/gpapi/device.properties)
|
||||
|
||||
To enable HTTPS through playmaker, without an external tool, just define `HTTPS_CERTFILE`
|
||||
and `HTTPS_KEYFILE` with paths to those file. If these variables are not set, tornado will default to http.
|
||||
|
||||
If you want to browse apps for a specific country, you need to specify the variables `LANG_LOCALE` and `LANG_TIMEZONE`.
|
||||
Before creating an issue "cannot find app X", make sure the app is available it that country.
|
||||
|
||||
The docker run command will look like this:
|
||||
```
|
||||
docker run -d --name playmaker \
|
||||
-p 5000:5000 \
|
||||
-v /srv/fdroid:/data/fdroid \
|
||||
-e HTTPS_CERTFILE="/srv/https.crt" \
|
||||
-e HTTPS_KEYFILE="/srv/https.key" \
|
||||
-e LANG_LOCALE="de_DE" \
|
||||
-e LANG_TIMEZONE="Europe/Berlin" \
|
||||
-e DEVICE_CODE="hammerhead" \
|
||||
fellek/playmaker:fellek
|
||||
```
|
||||
|
||||
If you want to run it in a virtualenv rather than using docker, remember that you need to install fdroidserver,
|
||||
android SDK and define the ANDROID\_HOME env variable (see the Dockerfile as a reference).
|
||||
Instruction on how to install fdroidserver [here](https://f-droid.org/docs/Installing_the_Server_and_Repo_Tools/)
|
||||
|
||||
## Alternatives
|
||||
|
||||
### YalpStore
|
||||
|
||||
[YalpStore](https://github.com/yeriomin/YalpStore) is an open source alternative to the play store.
|
||||
It works very well and it requires you to install only the app, but it requires one of the
|
||||
following thing to be able to install/update apks:
|
||||
|
||||
- enable **Unknown Sources**
|
||||
- have **root** privileges
|
||||
|
||||
If you use playmaker and the fdroid [privileged extension](https://gitlab.com/fdroid/privileged-extension),
|
||||
fdroid will be able to install/update app without root privileges or enabling unknown sources.
|
BIN
playmaker/example.png
Normal file
BIN
playmaker/example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
89
playmaker/playmaker/index.html
Normal file
89
playmaker/playmaker/index.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="playmaker">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Playmaker</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
|
||||
crossorigin="anonymous">
|
||||
<link rel="stylesheet"
|
||||
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
|
||||
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
|
||||
crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css"
|
||||
integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU"
|
||||
crossorigin="anonymous">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
|
||||
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.7.5/angular.min.js"></script>
|
||||
<link href="/static/app.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div ng-controller="navbar" class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse"
|
||||
data-target=".navbar-responsive-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button> <a class="navbar-brand">Playmaker</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse navbar-responsive-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li ng-class="{active: path === '/'}">
|
||||
<a href="/#!/" id="app-page-link">
|
||||
<i class="fab fa-android" aria-hidden="true"></i> Apps
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: path === '/search'}">
|
||||
<a href="/#!/search" id="search-page-link">
|
||||
<i class="fas fa-search" aria-hidden="true"></i> Search
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<a href=
|
||||
"https://github.com/NoMore201/playmaker/releases/tag/v0.6.4">v0.6.4</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/NoMore201/playmaker">
|
||||
<i class="fab fa-github" aria-hidden="true"></i> Source
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notification-panel" ng-controller="notify">
|
||||
<script type="text/ng-template" id="alert.html">
|
||||
<div ng-transclude></div>
|
||||
</script>
|
||||
<div uib-alert="" ng-repeat="alert in alerts" ng-class=
|
||||
"'alert-' + (alert.type || 'warning')" close="closeAlert($index)"
|
||||
dismiss-on-timeout="3000">
|
||||
{{alert.msg}}
|
||||
</div>
|
||||
</div>
|
||||
<div ng-view=""></div>
|
||||
|
||||
<!-- angular libraries -->
|
||||
<script src="/static/js/angular-route.min.js"></script>
|
||||
<script src="/static/js/angular-animate.min.js"></script>
|
||||
<script src="/static/js/angular-touch.min.js"></script>
|
||||
<script src="/static/js/ui-bootstrap.min.js"></script>
|
||||
<script src="/static/js/additional.js"></script>
|
||||
<!-- crypto library -->
|
||||
|
||||
<script src="/static/js/crypto-js.js"></script>
|
||||
|
||||
<script src="/static/app.module.js"></script>
|
||||
<script src="/static/app.service.js"></script>
|
||||
<script src="/static/app.controller.js"></script>
|
||||
</body>
|
||||
</html>
|
151
playmaker/playmaker/server.py
Normal file
151
playmaker/playmaker/server.py
Normal file
@ -0,0 +1,151 @@
|
||||
import os
|
||||
import tornado
|
||||
|
||||
from tornado import web
|
||||
from tornado.concurrent import run_on_executor
|
||||
from tornado.web import MissingArgumentError
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
app_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
static_dir = os.path.join(app_dir, 'static')
|
||||
fdroid_instance = {}
|
||||
|
||||
|
||||
def createServer(service):
|
||||
|
||||
class HomeHandler(web.RequestHandler):
|
||||
def get(self):
|
||||
with open(app_dir + '/index.html', 'r') as f:
|
||||
self.write(f.read())
|
||||
|
||||
class ApiHandler(web.RequestHandler):
|
||||
executor = ThreadPoolExecutor()
|
||||
|
||||
@run_on_executor
|
||||
def get_apps(self):
|
||||
return service.get_apps()
|
||||
|
||||
@run_on_executor
|
||||
def get_last_fdroid_update(self):
|
||||
return service.get_last_fdroid_update()
|
||||
|
||||
@run_on_executor
|
||||
def search(self):
|
||||
try:
|
||||
keyword = self.get_argument('search')
|
||||
except MissingArgumentError:
|
||||
return None
|
||||
return service.search(keyword)
|
||||
|
||||
@run_on_executor
|
||||
def login(self):
|
||||
data = tornado.escape.json_decode(self.request.body)
|
||||
service.set_encoded_credentials(data.get('email'), data.get('password'))
|
||||
return service.login()
|
||||
|
||||
@run_on_executor
|
||||
def download(self):
|
||||
data = tornado.escape.json_decode(self.request.body)
|
||||
if data.get('download') is None:
|
||||
return None
|
||||
return service.download_selection(data['download'])
|
||||
|
||||
@run_on_executor
|
||||
def check(self):
|
||||
return service.check_local_apks()
|
||||
|
||||
@run_on_executor
|
||||
def update_state(self):
|
||||
service.update_state()
|
||||
|
||||
@run_on_executor
|
||||
def remove_app(self, app):
|
||||
return service.remove_local_app(app)
|
||||
|
||||
@run_on_executor
|
||||
def update_fdroid(self):
|
||||
return service.fdroid_update()
|
||||
|
||||
@tornado.gen.coroutine
|
||||
def get(self, path):
|
||||
if path == 'apps':
|
||||
apps = yield self.get_apps()
|
||||
self.write(apps)
|
||||
elif path == 'search':
|
||||
apps = yield self.search()
|
||||
if apps is not None:
|
||||
self.write(apps)
|
||||
else:
|
||||
self.clear()
|
||||
self.set_status(400, 'You should supply a valid search query')
|
||||
elif path == 'fdroid':
|
||||
result = yield self.get_last_fdroid_update()
|
||||
self.write(result)
|
||||
else:
|
||||
self.set_status(404)
|
||||
self.finish()
|
||||
|
||||
@tornado.gen.coroutine
|
||||
def post(self, path):
|
||||
if path == 'download':
|
||||
result = yield self.download()
|
||||
if result is None:
|
||||
self.clear()
|
||||
self.set_status(400)
|
||||
else:
|
||||
self.write(result)
|
||||
elif path == 'check':
|
||||
result = yield self.check()
|
||||
self.write(result)
|
||||
elif path == 'login':
|
||||
result = yield self.login()
|
||||
self.write(result)
|
||||
if result['status'] == 'SUCCESS' and result['message'] == 'OK':
|
||||
self.update_state()
|
||||
elif path == 'fdroid':
|
||||
global fdroid_instance
|
||||
if fdroid_instance != {}:
|
||||
self.write({'status': 'PENDING'})
|
||||
else:
|
||||
fdroid_instance = self
|
||||
result = yield self.update_fdroid()
|
||||
self.write(result)
|
||||
fdroid_instance = {}
|
||||
else:
|
||||
self.set_status(404)
|
||||
self.finish()
|
||||
|
||||
@tornado.gen.coroutine
|
||||
def delete(self, path):
|
||||
if path == 'delete':
|
||||
data = tornado.escape.json_decode(self.request.body)
|
||||
if data.get('delete') is None:
|
||||
self.clear()
|
||||
self.set_status(400)
|
||||
else:
|
||||
result = yield self.remove_app(data['delete'])
|
||||
self.write(result)
|
||||
else:
|
||||
self.set_status(404)
|
||||
self.finish()
|
||||
|
||||
if service.fdroid:
|
||||
app = web.Application([
|
||||
(r'/', HomeHandler),
|
||||
(r'/api/(.*?)/?', ApiHandler),
|
||||
(r'/fdroid/(.*)', web.StaticFileHandler, {'path': service.download_path}),
|
||||
(r'/static/(.*)', web.StaticFileHandler, {'path': static_dir}),
|
||||
(r'/views/(.*)', web.StaticFileHandler, {'path': app_dir + '/views'}),
|
||||
], debug=False)
|
||||
else:
|
||||
app = web.Application([
|
||||
(r'/', HomeHandler),
|
||||
(r'/api/(.*?)/?', ApiHandler),
|
||||
(r'/static/(.*)', web.StaticFileHandler, {'path': static_dir}),
|
||||
(r'/views/(.*)', web.StaticFileHandler, {'path': app_dir + '/views'}),
|
||||
], debug=False)
|
||||
|
||||
# overwrite settings
|
||||
app.settings['static_path'] = ''
|
||||
return app
|
385
playmaker/playmaker/service.py
Normal file
385
playmaker/playmaker/service.py
Normal file
@ -0,0 +1,385 @@
|
||||
from gpapi.googleplay import GooglePlayAPI, LoginError, RequestError, SecurityCheckError
|
||||
from pyaxmlparser import APK
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
import concurrent.futures
|
||||
import locale as locale_service
|
||||
from datetime import datetime as dt
|
||||
|
||||
NOT_LOGGED_IN_ERR = 'Not logged in'
|
||||
WRONG_CREDENTIALS_ERR = 'Wrong credentials'
|
||||
SESSION_EXPIRED_ERR = 'Session tokens expired, re-login needed'
|
||||
FDROID_ERR = 'Error while executing fdroidserver tool'
|
||||
|
||||
|
||||
def makeError(message):
|
||||
return {'status': 'ERROR',
|
||||
'message': message}
|
||||
|
||||
|
||||
def get_details_from_apk(apk, downloadPath, service):
|
||||
if apk is not None:
|
||||
filepath = os.path.join(downloadPath, apk)
|
||||
try:
|
||||
a = APK(filepath)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
print('Fetching details for %s' % a.package)
|
||||
try:
|
||||
details = service.details(a.package)
|
||||
details['filename'] = apk
|
||||
details['versionCode'] = int(a.version_code)
|
||||
except RequestError as e:
|
||||
print('Cannot fetch information for %s' % a.package)
|
||||
print('Extracting basic information from package...')
|
||||
return {'docid': a.package,
|
||||
'filename': apk,
|
||||
'versionCode': int(a.version_code),
|
||||
'title': a.application}
|
||||
print('Added %s to cache' % details['docid'])
|
||||
return details
|
||||
|
||||
|
||||
class Play(object):
|
||||
def __init__(self, debug=True, fdroid=False):
|
||||
self.currentSet = []
|
||||
self.totalNumOfApps = 0
|
||||
self.debug = debug
|
||||
self.fdroid = fdroid
|
||||
self.firstRun = True
|
||||
self.loggedIn = False
|
||||
self._email = None
|
||||
self._passwd = None
|
||||
self._gsfId = None
|
||||
self._token = None
|
||||
self._last_fdroid_update = None
|
||||
|
||||
# configuring download folder
|
||||
if self.fdroid:
|
||||
self.download_path = os.path.join(os.getcwd(), 'repo')
|
||||
else:
|
||||
self.download_path = os.getcwd()
|
||||
|
||||
# configuring fdroid data
|
||||
if self.fdroid:
|
||||
self.fdroid_exe = 'fdroid'
|
||||
self.fdroid_path = os.getcwd()
|
||||
self.fdroid_init()
|
||||
|
||||
# language settings
|
||||
locale = os.environ.get('LANG_LOCALE')
|
||||
if locale is None:
|
||||
locale = locale_service.getdefaultlocale()[0]
|
||||
timezone = os.environ.get('LANG_TIMEZONE')
|
||||
if timezone is None:
|
||||
timezone = 'Europe/Berlin'
|
||||
device = os.environ.get('DEVICE_CODE')
|
||||
if device is None:
|
||||
self.service = GooglePlayAPI(locale, timezone)
|
||||
else:
|
||||
self.service = GooglePlayAPI(locale, timezone,
|
||||
device_codename=device)
|
||||
|
||||
def fdroid_init(self):
|
||||
found = False
|
||||
for path in os.environ['PATH'].split(':'):
|
||||
exe = os.path.join(path, self.fdroid_exe)
|
||||
if os.path.isfile(exe):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
print('Please install fdroid')
|
||||
sys.exit(1)
|
||||
elif os.path.isfile('config.py'):
|
||||
print('Repo already initalized, skipping init')
|
||||
else:
|
||||
p = Popen([self.fdroid_exe, 'init', '-v'], stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
sys.stderr.write("error initializing fdroid repository " +
|
||||
stderr.decode('utf-8'))
|
||||
sys.exit(1)
|
||||
# backup config.py
|
||||
if self.debug:
|
||||
print('Checking config.py file')
|
||||
with open('config.py', 'r') as config_file:
|
||||
content = config_file.readlines()
|
||||
with open('config.py', 'w') as config_file:
|
||||
# copy all the original content of config.py
|
||||
# if the file was not modified with custom values, do it
|
||||
modified = False
|
||||
for line in content:
|
||||
if '# playmaker' in line:
|
||||
modified = True
|
||||
config_file.write(line)
|
||||
if not modified:
|
||||
if self.debug:
|
||||
print('Appending playmaker data to config.py')
|
||||
config_file.write('\n# playmaker\nrepo_name = "playmaker"\n'
|
||||
'repo_description = "repository managed with '
|
||||
'playmaker https://github.com/NoMore201/playmaker"\n')
|
||||
|
||||
# ensure all folder and files are setup
|
||||
p = Popen([self.fdroid_exe, 'update', '--create-key', '-v'], stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
print('Skipping fdroid update')
|
||||
else:
|
||||
print('Fdroid repo initialized successfully')
|
||||
|
||||
def get_last_fdroid_update(self):
|
||||
if not self.loggedIn:
|
||||
return {'status': 'UNAUTHORIZED'}
|
||||
return {'status': 'SUCCESS',
|
||||
'message': str(self._last_fdroid_update)}
|
||||
|
||||
def fdroid_update(self):
|
||||
if not self.loggedIn:
|
||||
return {'status': 'UNAUTHORIZED'}
|
||||
if self.fdroid:
|
||||
try:
|
||||
p = Popen([self.fdroid_exe, 'update', '-c', '--clean'],
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
sys.stderr.write("error updating fdroid repository " +
|
||||
stderr.decode('utf-8'))
|
||||
return makeError(FDROID_ERR)
|
||||
else:
|
||||
print('Fdroid repo updated successfully')
|
||||
self._last_fdroid_update = dt.today().replace(microsecond=0)
|
||||
return {'status': 'SUCCESS'}
|
||||
except Exception as e:
|
||||
return makeError(FDROID_ERR)
|
||||
else:
|
||||
return {'status': 'SUCCESS'}
|
||||
|
||||
def get_apps(self):
|
||||
if not self.loggedIn:
|
||||
return {'status': 'UNAUTHORIZED'}
|
||||
|
||||
if self.firstRun:
|
||||
return {'status': 'PENDING',
|
||||
'total': self.totalNumOfApps,
|
||||
'current': len(self.currentSet)}
|
||||
return {'status': 'SUCCESS',
|
||||
'message': sorted(self.currentSet, key=lambda k: k['title'])}
|
||||
|
||||
def set_encoded_credentials(self, email, password):
|
||||
self._email = base64.b64decode(email).decode('utf-8')
|
||||
self._passwd = base64.b64decode(password).decode('utf-8')
|
||||
|
||||
def set_credentials(self, email, password):
|
||||
self._email = email
|
||||
self._passwd = password
|
||||
|
||||
def set_token_credentials(self, gsfId, token):
|
||||
self._gsfId = int(gsfId, 16)
|
||||
self._token = token
|
||||
|
||||
def has_credentials(self):
|
||||
passwd_credentials = self._email is not None and self._passwd is not None
|
||||
token_credentials = self._gsfId is not None and self._token is not None
|
||||
return passwd_credentials or token_credentials
|
||||
|
||||
def login(self):
|
||||
if self.loggedIn:
|
||||
return {'status': 'SUCCESS', 'securityCheck': False, 'message': 'OK'}
|
||||
|
||||
try:
|
||||
if not self.has_credentials():
|
||||
raise LoginError("missing credentials")
|
||||
self.service.login(self._email,
|
||||
self._passwd,
|
||||
self._gsfId,
|
||||
self._token)
|
||||
self.loggedIn = True
|
||||
return {'status': 'SUCCESS', 'securityCheck': False, 'message': 'OK'}
|
||||
except LoginError as e:
|
||||
print('LoginError: {0}'.format(e))
|
||||
self.loggedIn = False
|
||||
return {'status': 'ERROR',
|
||||
'securityCheck': False,
|
||||
'message': 'Wrong credentials'}
|
||||
except SecurityCheckError as e:
|
||||
print('SecurityCheckError: {0}'.format(e))
|
||||
self.loggedIn = False
|
||||
return {'status': 'ERROR',
|
||||
'securityCheck': True,
|
||||
'message': 'Need security check'}
|
||||
except RequestError as e:
|
||||
# probably tokens are invalid, so it is better to
|
||||
# invalidate them
|
||||
print('RequestError: {0}'.format(e))
|
||||
self.loggedIn = False
|
||||
return {'status': 'ERROR',
|
||||
'securityCheck': False,
|
||||
'message': 'Request error, probably invalid token'}
|
||||
|
||||
def update_state(self):
|
||||
if not self.loggedIn:
|
||||
return {'status': 'UNAUTHORIZED'}
|
||||
|
||||
print('Updating cache')
|
||||
with concurrent.futures.ProcessPoolExecutor() as executor:
|
||||
# get application ids from apk files
|
||||
apkFiles = [apk for apk in os.listdir(self.download_path)
|
||||
if os.path.splitext(apk)[1] == '.apk']
|
||||
self.totalNumOfApps = len(apkFiles)
|
||||
if self.totalNumOfApps != 0:
|
||||
future_to_app = [executor.submit(get_details_from_apk,
|
||||
a,
|
||||
self.download_path,
|
||||
self.service)
|
||||
for a in apkFiles]
|
||||
for future in concurrent.futures.as_completed(future_to_app):
|
||||
app = future.result()
|
||||
if app is not None:
|
||||
self.currentSet.append(app)
|
||||
print('Cache correctly initialized')
|
||||
self.firstRun = False
|
||||
|
||||
def insert_app_into_state(self, newApp):
|
||||
found = False
|
||||
result = list(filter(lambda x: x['docid'] == newApp['docid'],
|
||||
self.currentSet))
|
||||
if len(result) > 0:
|
||||
found = True
|
||||
if self.debug:
|
||||
print('%s is already cached, updating..' % newApp['docid'])
|
||||
i = self.currentSet.index(result[0])
|
||||
self.currentSet[i] = newApp
|
||||
if not found:
|
||||
if self.debug:
|
||||
print('Adding %s into cache..' % newApp['docid'])
|
||||
self.currentSet.append(newApp)
|
||||
|
||||
def search(self, appName, numItems=15):
|
||||
if not self.loggedIn:
|
||||
return {'status': 'UNAUTHORIZED'}
|
||||
|
||||
try:
|
||||
apps = self.service.search(appName)
|
||||
except RequestError as e:
|
||||
print(e)
|
||||
self.loggedIn = False
|
||||
return {'status': 'ERROR',
|
||||
'message': SESSION_EXPIRED_ERR}
|
||||
except LoginError as e:
|
||||
print(SESSION_EXPIRED_ERR)
|
||||
self.loggedIn = False
|
||||
except IndexError as e:
|
||||
print(SESSION_EXPIRED_ERR)
|
||||
self.loggedIn = False
|
||||
|
||||
return {'status': 'SUCCESS',
|
||||
'message': apps}
|
||||
|
||||
def details(self, app):
|
||||
try:
|
||||
details = self.service.details(app)
|
||||
except RequestError:
|
||||
details = None
|
||||
return details
|
||||
|
||||
def get_bulk_details(self, apksList):
|
||||
if not self.loggedIn:
|
||||
return {'status': 'UNAUTHORIZED'}
|
||||
try:
|
||||
apps = [self.details(a) for a in apksList]
|
||||
except LoginError as e:
|
||||
print(e)
|
||||
self.loggedIn = False
|
||||
return apps
|
||||
|
||||
def download_selection(self, apps):
|
||||
if not self.loggedIn:
|
||||
return {'status': 'UNAUTHORIZED'}
|
||||
success = []
|
||||
failed = []
|
||||
unavail = []
|
||||
|
||||
for app in apps:
|
||||
docid = app.get('docid')
|
||||
details = self.details(docid)
|
||||
filename = app.get('filename')
|
||||
if filename is None:
|
||||
filename = details.get('docid') + '.apk'
|
||||
if details is None:
|
||||
print('Package %s does not exits' % docid)
|
||||
unavail.append(docid)
|
||||
continue
|
||||
print('Downloading %s' % docid)
|
||||
try:
|
||||
if details.get('offer')[0].get('micros') == 0:
|
||||
data_gen = self.service.download(docid, details.get('details').get('appDetails')['versionCode'])
|
||||
else:
|
||||
data_gen = self.service.delivery(docid, details.get('details').get('appDetails')['versionCode'])
|
||||
data_gen = data_gen.get('file').get('data')
|
||||
except IndexError as exc:
|
||||
print(exc)
|
||||
print('Package %s does not exists' % docid)
|
||||
unavail.append(docid)
|
||||
except Exception as exc:
|
||||
print(exc)
|
||||
print('Failed to download %s' % docid)
|
||||
failed.append(docid)
|
||||
else:
|
||||
filepath = os.path.join(self.download_path, filename)
|
||||
try:
|
||||
with open(filepath, 'wb') as apk_file:
|
||||
for chunk in data_gen:
|
||||
apk_file.write(chunk)
|
||||
except IOError as exc:
|
||||
print('Error while writing %s: %s' % (filename, exc))
|
||||
failed.append(docid)
|
||||
details['filename'] = filename
|
||||
success.append(details)
|
||||
for x in success:
|
||||
self.insert_app_into_state(x)
|
||||
return {'status': 'SUCCESS',
|
||||
'message': {'success': success,
|
||||
'failed': failed,
|
||||
'unavail': unavail}}
|
||||
|
||||
def check_local_apks(self):
|
||||
if not self.loggedIn:
|
||||
return {'status': 'UNAUTHORIZED'}
|
||||
if len(self.currentSet) == 0:
|
||||
print('There is no package')
|
||||
return {'status': 'SUCCESS',
|
||||
'message': []}
|
||||
else:
|
||||
toUpdate = []
|
||||
for app in self.currentSet:
|
||||
details = self.details(app.get('docid'))
|
||||
#print(details)
|
||||
if details is None:
|
||||
print('%s not available in Play Store' % app['docid'])
|
||||
continue
|
||||
details['filename'] = app.get('filename')
|
||||
if self.debug:
|
||||
print('Checking %s' % app['docid'])
|
||||
print('%d == %d ?' % (app.get('details').get('appDetails')['versionCode'], details.get('details').get('appDetails')['versionCode']))
|
||||
if app.get('details').get('appDetails')['versionCode'] != details.get('details').get('appDetails')['versionCode']:
|
||||
toUpdate.append(details)
|
||||
return {'status': 'SUCCESS',
|
||||
'message': toUpdate}
|
||||
|
||||
def remove_local_app(self, docid):
|
||||
if not self.loggedIn:
|
||||
return {'status': 'UNAUTHORIZED'}
|
||||
# get app from cache
|
||||
app = list(filter(lambda x: x['docid'] == docid, self.currentSet))
|
||||
if len(app) < 1:
|
||||
return {'status': 'ERROR'}
|
||||
apkPath = os.path.join(self.download_path, app[0]['filename'])
|
||||
if os.path.isfile(apkPath):
|
||||
os.remove(apkPath)
|
||||
self.currentSet.remove(app[0])
|
||||
return {'status': 'SUCCESS'}
|
||||
return {'status': 'ERROR'}
|
28
playmaker/playmaker/static/app.controller.js
Normal file
28
playmaker/playmaker/static/app.controller.js
Normal file
@ -0,0 +1,28 @@
|
||||
angular.module('playmaker').controller('notify', [
|
||||
'$scope',
|
||||
'global',
|
||||
function($scope, global) {
|
||||
$scope.alerts = [];
|
||||
|
||||
$scope.closeAlert = function(index) {
|
||||
$scope.alerts.splice(index, 1);
|
||||
};
|
||||
|
||||
global.addAlert = function(type, msg) {
|
||||
newAlert = {
|
||||
type: type,
|
||||
msg: msg
|
||||
};
|
||||
$scope.alerts.push(newAlert);
|
||||
};
|
||||
}]);
|
||||
|
||||
angular.module('playmaker').controller('navbar', [
|
||||
'$location',
|
||||
'$scope',
|
||||
'$rootScope',
|
||||
function($location, $scope, $rootScope) {
|
||||
$rootScope.$on('$routeChangeSuccess', function() {
|
||||
$scope.path = $location.path();
|
||||
});
|
||||
}]);
|
177
playmaker/playmaker/static/app.css
Normal file
177
playmaker/playmaker/static/app.css
Normal file
@ -0,0 +1,177 @@
|
||||
/*
|
||||
* index.html
|
||||
*/
|
||||
|
||||
.navbar-fixed-top {
|
||||
top:0;
|
||||
}
|
||||
|
||||
.notification-panel {
|
||||
position: fixed;
|
||||
right: 30px;
|
||||
top: 75px;
|
||||
width: 300px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/*
|
||||
* Login view
|
||||
*/
|
||||
|
||||
.login-body {
|
||||
margin:0 auto;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.loading-login {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.loading-login-text {
|
||||
font-size: 20px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Apps view
|
||||
*/
|
||||
|
||||
.sidebar {
|
||||
margin: 0 auto;
|
||||
margin-top: 71px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.sidebar-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-list-el {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height:50px;
|
||||
justify-content: space-between;
|
||||
padding-left:15px;
|
||||
padding-right:15px;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.apk-item-title {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
margin-bottom: 12px;
|
||||
margin-top:10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.apk-info {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.apk-buttons {
|
||||
display: block;
|
||||
padding:15px;
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
.apk-buttons > a {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.apk-progress {
|
||||
margin:24px 15px 18px 15px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Search view
|
||||
*/
|
||||
|
||||
.view-container {
|
||||
max-width: 1000px;
|
||||
margin:0 auto;
|
||||
margin-top: 81px;
|
||||
padding:0 15px;
|
||||
}
|
||||
|
||||
.dl-button {
|
||||
cursor: pointer;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.dl-button:hover {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.dl-button-disabled {
|
||||
cursor: default;
|
||||
pointer-event: none;
|
||||
color: #BABABA;
|
||||
}
|
||||
|
||||
.dl-button-disabled:hover {
|
||||
color: #BABABA;
|
||||
}
|
||||
|
||||
#dl-button-td {
|
||||
width:80px;
|
||||
}
|
||||
|
||||
#search-progress {
|
||||
margin-top:40px;
|
||||
margin-left:30px;
|
||||
margin-right:30px;
|
||||
}
|
||||
|
||||
#search-area {
|
||||
padding:0 10px;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top:40px;
|
||||
}
|
||||
|
||||
#table-body > tr {
|
||||
line-height: 70px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#table-body > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.sidebar {
|
||||
background-color:#f7f7f7;
|
||||
bottom:0;
|
||||
left:0;
|
||||
margin:0;
|
||||
padding-top:30px;
|
||||
position: fixed;
|
||||
top: 51px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
margin-left:300px;
|
||||
margin-top: 81px;
|
||||
max-width: 1200px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
347
playmaker/playmaker/static/app.module.js
Normal file
347
playmaker/playmaker/static/app.module.js
Normal file
@ -0,0 +1,347 @@
|
||||
var app = angular.module('playmaker', [
|
||||
'ngRoute',
|
||||
'ui.bootstrap'
|
||||
]);
|
||||
|
||||
app.config(['$locationProvider', '$routeProvider',
|
||||
function config($locationProvider, $routeProvider) {
|
||||
|
||||
$routeProvider.
|
||||
when('/', {
|
||||
template: '<app-list></app-list>'
|
||||
}).
|
||||
when('/search', {
|
||||
template: '<search-view></search-view>'
|
||||
}).
|
||||
when('/login', {
|
||||
template: '<login-view></login-view>'
|
||||
}).
|
||||
otherwise('/');
|
||||
}
|
||||
|
||||
]).run(['$rootScope', '$location', 'api', 'global',
|
||||
function ($rootScope, $location, api, global) {
|
||||
|
||||
api.getApps(function(response) {
|
||||
if (response.status === 'SUCCESS') {
|
||||
global.auth.login();
|
||||
}
|
||||
|
||||
if (response === 'err' || !global.auth.isLoggedIn()) {
|
||||
$location.path('/login');
|
||||
} else {
|
||||
// redirect home
|
||||
$location.path('/');
|
||||
}
|
||||
|
||||
$rootScope.$on('$routeChangeStart', function (event, next, current) {
|
||||
if (!global.auth.isLoggedIn() && $location.path() !== '/login') {
|
||||
event.preventDefault();
|
||||
$location.path('/login');
|
||||
} else if (global.auth.isLoggedIn() && $location.path() === '/login') {
|
||||
// redirect home
|
||||
event.preventDefault();
|
||||
$location.path('/');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
]);
|
||||
|
||||
app.component('appList', {
|
||||
templateUrl: '/views/app.html',
|
||||
controller: function AppController(api, global, $location) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.apps = [];
|
||||
ctrl.lastFdroidUpdate = 'None';
|
||||
ctrl.desktop = global.desktop;
|
||||
ctrl.mobile = global.mobile;
|
||||
var port = $location.port();
|
||||
ctrl.baseUrl = $location.protocol() + '://' + $location.host();
|
||||
if (port !== 80 && port !== 443) {
|
||||
ctrl.baseUrl += ":" + port.toString();
|
||||
}
|
||||
|
||||
var updateApp = function(app) {
|
||||
app.updating = true;
|
||||
api.download(app, function(data) {
|
||||
if (data === 'err' || data.status === 'ERROR') {
|
||||
global.addAlert('danger', 'Unable to update ' + app.docid);
|
||||
app.updating = false;
|
||||
return;
|
||||
}
|
||||
if (data.message.success.length === 0) {
|
||||
global.addAlert('danger', 'Unable to update ' + app.docid);
|
||||
app.updating = false;
|
||||
return;
|
||||
}
|
||||
app.versionCode = data.message.success[0].versionCode;
|
||||
app.updating = false;
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.check = function() {
|
||||
global.addAlert('info', 'Checking for updates');
|
||||
api.check(function(data) {
|
||||
if (data === 'err') {
|
||||
global.addAlert('danger', 'Cannot check for updates');
|
||||
return;
|
||||
}
|
||||
if (data.status === 'SUCCESS' && data.message.length === 0) {
|
||||
global.addAlert('success', 'All apps are up-to-date!');
|
||||
}
|
||||
if (data.status === 'SUCCESS' && data.message.length > 0) {
|
||||
global.addAlert('success', 'Updating ' + data.message.length.toString() + ' apps');
|
||||
|
||||
data.message.forEach(function(newApp) {
|
||||
var oldAppIndex = ctrl.apps.findIndex(function(elem) {
|
||||
return elem.docid === newApp.docid
|
||||
});
|
||||
if (oldAppIndex === -1) return;
|
||||
updateApp(ctrl.apps[oldAppIndex]);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
ctrl.delete = function(app) {
|
||||
api.remove(app.docid, function(data) {
|
||||
if (data.status === 'SUCCESS') {
|
||||
var i = ctrl.apps.findIndex(function(elem) {
|
||||
return elem.docid === app.docid;
|
||||
});
|
||||
ctrl.apps.splice(i, 1);
|
||||
} else {
|
||||
global.addAlert('danger', 'Unable to delete ' + app.docid);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.fdroid = function() {
|
||||
var oldUpdate = ctrl.lastFdroidUpdate;
|
||||
ctrl.lastFdroidUpdate = 'Pending';
|
||||
api.fdroidUpdate(function (data) {
|
||||
if (data === 'err') {
|
||||
global.addAlert('danger', 'Error updating repository');
|
||||
ctrl.lastFdroidUpdate = oldUpdate;
|
||||
return;
|
||||
}
|
||||
if (data.status === 'PENDING') {
|
||||
return;
|
||||
}
|
||||
if (data.status === 'SUCCESS') {
|
||||
api.fdroid(function(data) {
|
||||
if (data.status !== 'SUCCESS') {
|
||||
return;
|
||||
}
|
||||
ctrl.lastFdroidUpdate = data.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
api.getApps(function(data) {
|
||||
if (data.status === 'UNAUTHORIZED') {
|
||||
return;
|
||||
}
|
||||
ctrl.apps = data.message.map(function(a) {
|
||||
if (a.aggregateRating !== undefined) {
|
||||
roundedStars = Math.floor(a.aggregateRating.starRating);
|
||||
a.formattedStars = a.aggregateRating.starRating.toFixed(1);
|
||||
a.starList = [];
|
||||
for (i = 0; i < 5; i++) {
|
||||
if (i+1 <= roundedStars){
|
||||
a.starList.push({index: i, full: true});
|
||||
} else {
|
||||
a.starList.push({index: i, full: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a.image !== undefined) {
|
||||
a.previewImage = a.image.filter(function(img) {
|
||||
return img.imageType === 4;
|
||||
});
|
||||
}
|
||||
if (a.details.appDetails.installationSize !== undefined) {
|
||||
a.formattedSize = a.details.appDetails.installationSize / (1024*1024);
|
||||
a.formattedSize = a.formattedSize.toFixed(2);
|
||||
}
|
||||
if (a.author === undefined) {
|
||||
a.author = "unknown";
|
||||
}
|
||||
if (a.files === undefined) {
|
||||
a.files = ["unknown"];
|
||||
}
|
||||
a.updating = false;
|
||||
return a;
|
||||
});
|
||||
});
|
||||
|
||||
api.fdroid(function(data) {
|
||||
if (data.status !== 'SUCCESS') {
|
||||
return;
|
||||
}
|
||||
ctrl.lastFdroidUpdate = data.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.directive('onEnter', function() {
|
||||
return function(scope, element, attrs) {
|
||||
element.bind("keydown keypress", function (event) {
|
||||
if(event.which === 13) {
|
||||
scope.$apply(function (){
|
||||
scope.$eval(attrs.onEnter);
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
app.component('searchView', {
|
||||
templateUrl: '/views/search.html',
|
||||
controller: function SearchController($uibModal, api, global) {
|
||||
var ctrl = this;
|
||||
ctrl.desktop = global.desktop;
|
||||
ctrl.mobile = global.mobile;
|
||||
ctrl.results = [];
|
||||
ctrl.searching = false;
|
||||
|
||||
ctrl.modalOpen = function (item) {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
ariaLabelledBy: 'modal-title',
|
||||
ariaDescribedBy: 'modal-body',
|
||||
templateUrl: 'myModalContent.html',
|
||||
controller: function($scope) {
|
||||
$scope.app = item;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.search = function(app) {
|
||||
// no input by the user
|
||||
if (app === undefined || app === '') return;
|
||||
ctrl.results = [];
|
||||
ctrl.searching = true;
|
||||
api.search(app, function(data) {
|
||||
if (data === 'err') {
|
||||
global.addAlert('danger', 'Error while searching');
|
||||
ctrl.searching = false;
|
||||
return;
|
||||
}
|
||||
if (data.status === 'SUCCESS' && data.message.length === 0) {
|
||||
global.addAlert('warning', 'No result for "' + app + '"');
|
||||
ctrl.searching = false;
|
||||
return;
|
||||
}
|
||||
data.message.forEach(function(d) {
|
||||
d.downloading = false;
|
||||
d.disabled = false;
|
||||
});
|
||||
ctrl.results = data.message[0].child[0].child;
|
||||
ctrl.searching = false;
|
||||
});
|
||||
};
|
||||
|
||||
ctrl.download = function(app) {
|
||||
if (app.disabled) {
|
||||
return;
|
||||
}
|
||||
app.downloading = true;
|
||||
api.download(app, function(data) {
|
||||
if (data === 'err') {
|
||||
app.downloading = false;
|
||||
global.addAlert('danger', 'Error downloading app');
|
||||
return;
|
||||
}
|
||||
if (data.status === 'SUCCESS') {
|
||||
if (data.message.success.length === 0) {
|
||||
app.downloading = false;
|
||||
global.addAlert('warning', app.docid + ' can\'t be downloaded');
|
||||
return;
|
||||
}
|
||||
}
|
||||
app.downloading = false;
|
||||
app.disabled = true;
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
app.component('loginView', {
|
||||
templateUrl: '/views/login.html',
|
||||
controller: function LoginController($location, api, global) {
|
||||
var ctrl = this;
|
||||
ctrl.current = 0;
|
||||
ctrl.max = -1;
|
||||
ctrl.formattedPercent = 0;
|
||||
ctrl.securityCheck = false;
|
||||
var polling = function() {
|
||||
api.getApps(function(response) {
|
||||
if (response === 'err') {
|
||||
ctrl.loggingIn = false;
|
||||
return;
|
||||
}
|
||||
if (response.status === 'UNAUTHORIZED') {
|
||||
return;
|
||||
}
|
||||
if (response.status === 'PENDING') {
|
||||
ctrl.loggingIn = true;
|
||||
if (response.total !== 0) {
|
||||
ctrl.max = response.total;
|
||||
ctrl.current = response.current;
|
||||
ctrl.formattedPercent = (ctrl.current / ctrl.max)*100;
|
||||
ctrl.formattedPercent = ctrl.formattedPercent.toFixed(1);
|
||||
}
|
||||
}
|
||||
if (response.status === 'SUCCESS') {
|
||||
global.auth.login();
|
||||
$location.path('/');
|
||||
ctrl.loggingIn = false;
|
||||
clearInterval(interval);
|
||||
}
|
||||
});
|
||||
};
|
||||
ctrl.loggingIn = false;
|
||||
ctrl.badUsername = false;
|
||||
ctrl.badPassword = false;
|
||||
|
||||
polling();
|
||||
var interval = setInterval(polling, 3000);
|
||||
|
||||
|
||||
ctrl.login = function(user) {
|
||||
ctrl.badUsername = false;
|
||||
ctrl.badPassword = false;
|
||||
|
||||
if (user.email === '' || user.email === undefined) {
|
||||
ctrl.badUsername = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.password === '' || user.password === undefined) {
|
||||
ctrl.badPassword = true;
|
||||
return;
|
||||
}
|
||||
|
||||
ctrl.loggingIn = true;
|
||||
var email = CryptoJS.enc.Utf8.parse(user.email);
|
||||
var passwd = CryptoJS.enc.Utf8.parse(user.password);
|
||||
var emailB64 = CryptoJS.enc.Base64.stringify(email);
|
||||
var passwdB64 = CryptoJS.enc.Base64.stringify(passwd);
|
||||
api.login(emailB64, passwdB64, function(data) {
|
||||
if (data.status === 'ERROR') {
|
||||
global.addAlert('danger', data.message);
|
||||
ctrl.loggingIn = false;
|
||||
ctrl.securityCheck = data.securityCheck;
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
150
playmaker/playmaker/static/app.service.js
Normal file
150
playmaker/playmaker/static/app.service.js
Normal file
@ -0,0 +1,150 @@
|
||||
angular.module('playmaker').service('global', ['$http', function($http) {
|
||||
|
||||
function AuthManager() {
|
||||
|
||||
this.loggedIn = false;
|
||||
|
||||
this.isLoggedIn = function () {
|
||||
return this.loggedIn;
|
||||
};
|
||||
|
||||
this.login = function () {
|
||||
this.loggedIn = !this.loggedIn;
|
||||
};
|
||||
}
|
||||
|
||||
this.addAlert = {};
|
||||
|
||||
this.desktop = false;
|
||||
this.mobile = false;
|
||||
|
||||
this.auth = new AuthManager();
|
||||
|
||||
var screenWidth = window.innerWidth;
|
||||
if (screenWidth < 700) {
|
||||
this.mobile = true;
|
||||
} else {
|
||||
this.desktop = true;
|
||||
}
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
angular.module('playmaker').service('api', ['$http', '$location', 'global', function($http, $location, global) {
|
||||
|
||||
function loginHandler(result) {
|
||||
if (result.data.status === 'ERROR') {
|
||||
if (result.data.message !== undefined) {
|
||||
global.addAlert('danger', result.data.message);
|
||||
} else {
|
||||
global.addAlert('danger', 'Application error');
|
||||
}
|
||||
global.auth.loggedIn = false;
|
||||
$location.path('/login');
|
||||
}
|
||||
}
|
||||
|
||||
this.getApps = function(callback) {
|
||||
$http({
|
||||
method: 'GET',
|
||||
url: '/api/apps'
|
||||
}).then(function success(response) {
|
||||
callback(response.data);
|
||||
}, function error(response) {
|
||||
callback('err');
|
||||
});
|
||||
};
|
||||
|
||||
this.search = function(app, callback) {
|
||||
$http({
|
||||
method: 'GET',
|
||||
url: '/api/search?search=' + app
|
||||
}).then(function success(response) {
|
||||
loginHandler(response);
|
||||
callback(response.data);
|
||||
}, function error(response) {
|
||||
callback('err');
|
||||
});
|
||||
};
|
||||
|
||||
this.check = function(callback) {
|
||||
$http.post('/api/check')
|
||||
.then(function success(response) {
|
||||
loginHandler(response);
|
||||
callback(response.data);
|
||||
}, function error(response) {
|
||||
callback('err');
|
||||
});
|
||||
};
|
||||
|
||||
this.download = function(app, callback) {
|
||||
var requestData = {
|
||||
download: [app]
|
||||
};
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: '/api/download',
|
||||
data: JSON.stringify(requestData)
|
||||
}).then(function success(response) {
|
||||
callback(response.data);
|
||||
}, function error(response) {
|
||||
callback('err');
|
||||
});
|
||||
};
|
||||
|
||||
this.remove = function(app, callback) {
|
||||
var requestData = {
|
||||
delete: app
|
||||
};
|
||||
$http({
|
||||
method: 'DELETE',
|
||||
url: '/api/delete',
|
||||
data: JSON.stringify(requestData)
|
||||
}).then(function success(response) {
|
||||
loginHandler(response);
|
||||
callback(response.data);
|
||||
}, function error(response) {
|
||||
callback('err');
|
||||
});
|
||||
};
|
||||
|
||||
this.fdroid = function(callback) {
|
||||
$http({
|
||||
method: 'GET',
|
||||
url: '/api/fdroid'
|
||||
}).then(function success(response) {
|
||||
callback(response.data);
|
||||
}, function error(response) {
|
||||
callback('err');
|
||||
});
|
||||
};
|
||||
|
||||
this.fdroidUpdate = function(callback) {
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: '/api/fdroid'
|
||||
}).then(function success(response) {
|
||||
loginHandler(response);
|
||||
callback(response.data);
|
||||
}, function error(response) {
|
||||
callback('err');
|
||||
});
|
||||
};
|
||||
|
||||
this.login = function(email, password, callback) {
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: '/api/login',
|
||||
data: JSON.stringify({
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
}).then(function success(response) {
|
||||
callback(response.data);
|
||||
}, function error(response) {
|
||||
callback('err');
|
||||
});
|
||||
};
|
||||
|
||||
}]);
|
||||
|
5
playmaker/playmaker/static/js/additional.js
Normal file
5
playmaker/playmaker/static/js/additional.js
Normal file
@ -0,0 +1,5 @@
|
||||
$(document).on('click', '.navbar-collapse.in', function(e) {
|
||||
if($(e.target).is('a')) {
|
||||
$(this).collapse('hide');
|
||||
}
|
||||
});
|
58
playmaker/playmaker/static/js/angular-animate.min.js
vendored
Normal file
58
playmaker/playmaker/static/js/angular-animate.min.js
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
AngularJS v1.7.5
|
||||
(c) 2010-2018 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(Y,z){'use strict';function Fa(a,b,c){if(!a)throw Pa("areq",b||"?",c||"required");return a}function Ga(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;Z(a)&&(a=a.join(" "));Z(b)&&(b=b.join(" "));return a+" "+b}function Qa(a){var b={};a&&(a.to||a.from)&&(b.to=a.to,b.from=a.from);return b}function $(a,b,c){var d="";a=Z(a)?a:a&&G(a)&&a.length?a.split(/\s+/):[];s(a,function(a,k){a&&0<a.length&&(d+=0<k?" ":"",d+=c?b+a:a+b)});return d}function Ha(a){if(a instanceof A)switch(a.length){case 0:return a;
|
||||
case 1:if(1===a[0].nodeType)return a;break;default:return A(va(a))}if(1===a.nodeType)return A(a)}function va(a){if(!a[0])return a;for(var b=0;b<a.length;b++){var c=a[b];if(1===c.nodeType)return c}}function Ra(a,b,c){s(b,function(b){a.addClass(b,c)})}function Sa(a,b,c){s(b,function(b){a.removeClass(b,c)})}function aa(a){return function(b,c){c.addClass&&(Ra(a,b,c.addClass),c.addClass=null);c.removeClass&&(Sa(a,b,c.removeClass),c.removeClass=null)}}function pa(a){a=a||{};if(!a.$$prepared){var b=a.domOperation||
|
||||
N;a.domOperation=function(){a.$$domOperationFired=!0;b();b=N};a.$$prepared=!0}return a}function ha(a,b){Ia(a,b);Ja(a,b)}function Ia(a,b){b.from&&(a.css(b.from),b.from=null)}function Ja(a,b){b.to&&(a.css(b.to),b.to=null)}function T(a,b,c){var d=b.options||{};c=c.options||{};var f=(d.addClass||"")+" "+(c.addClass||""),k=(d.removeClass||"")+" "+(c.removeClass||"");a=Ta(a.attr("class"),f,k);c.preparationClasses&&(d.preparationClasses=ba(c.preparationClasses,d.preparationClasses),delete c.preparationClasses);
|
||||
f=d.domOperation!==N?d.domOperation:null;wa(d,c);f&&(d.domOperation=f);d.addClass=a.addClass?a.addClass:null;d.removeClass=a.removeClass?a.removeClass:null;b.addClass=d.addClass;b.removeClass=d.removeClass;return d}function Ta(a,b,c){function d(a){G(a)&&(a=a.split(" "));var c={};s(a,function(a){a.length&&(c[a]=!0)});return c}var f={};a=d(a);b=d(b);s(b,function(a,c){f[c]=1});c=d(c);s(c,function(a,c){f[c]=1===f[c]?null:-1});var k={addClass:"",removeClass:""};s(f,function(c,b){var d,f;1===c?(d="addClass",
|
||||
f=!a[b]||a[b+"-remove"]):-1===c&&(d="removeClass",f=a[b]||a[b+"-add"]);f&&(k[d].length&&(k[d]+=" "),k[d]+=b)});return k}function K(a){return a instanceof A?a[0]:a}function Ua(a,b,c,d){a="";c&&(a=$(c,"ng-",!0));d.addClass&&(a=ba(a,$(d.addClass,"-add")));d.removeClass&&(a=ba(a,$(d.removeClass,"-remove")));a.length&&(d.preparationClasses=a,b.addClass(a))}function qa(a,b){var c=b?"-"+b+"s":"";ma(a,[na,c]);return[na,c]}function xa(a,b){var c=b?"paused":"",d=ca+"PlayState";ma(a,[d,c]);return[d,c]}function ma(a,
|
||||
b){a.style[b[0]]=b[1]}function ba(a,b){return a?b?a+" "+b:a:b}function Ka(a,b,c){var d=Object.create(null),f=a.getComputedStyle(b)||{};s(c,function(a,c){var b=f[a];if(b){var L=b.charAt(0);if("-"===L||"+"===L||0<=L)b=Va(b);0===b&&(b=null);d[c]=b}});return d}function Va(a){var b=0;a=a.split(/\s*,\s*/);s(a,function(a){"s"===a.charAt(a.length-1)&&(a=a.substring(0,a.length-1));a=parseFloat(a)||0;b=b?Math.max(a,b):a});return b}function ya(a){return 0===a||null!=a}function La(a,b){var c=M,d=a+"s";b?c+="Duration":
|
||||
d+=" linear all";return[c,d]}function Ma(a,b,c){s(c,function(c){a[c]=za(a[c])?a[c]:b.style.getPropertyValue(c)})}var M,Aa,ca,Ba;void 0===Y.ontransitionend&&void 0!==Y.onwebkittransitionend?(M="WebkitTransition",Aa="webkitTransitionEnd transitionend"):(M="transition",Aa="transitionend");void 0===Y.onanimationend&&void 0!==Y.onwebkitanimationend?(ca="WebkitAnimation",Ba="webkitAnimationEnd animationend"):(ca="animation",Ba="animationend");var ra=ca+"Delay",Ca=ca+"Duration",na=M+"Delay",Na=M+"Duration",
|
||||
Pa=z.$$minErr("ng"),Wa={transitionDuration:Na,transitionDelay:na,transitionProperty:M+"Property",animationDuration:Ca,animationDelay:ra,animationIterationCount:ca+"IterationCount"},Xa={transitionDuration:Na,transitionDelay:na,animationDuration:Ca,animationDelay:ra},Da,wa,s,Z,za,sa,Ea,ta,G,R,A,N;z.module("ngAnimate",[],function(){N=z.noop;Da=z.copy;wa=z.extend;A=z.element;s=z.forEach;Z=z.isArray;G=z.isString;ta=z.isObject;R=z.isUndefined;za=z.isDefined;Ea=z.isFunction;sa=z.isElement}).info({angularVersion:"1.7.5"}).directive("ngAnimateSwap",
|
||||
["$animate",function(a){return{restrict:"A",transclude:"element",terminal:!0,priority:600,link:function(b,c,d,f,k){var e,Q;b.$watchCollection(d.ngAnimateSwap||d["for"],function(b){e&&a.leave(e);Q&&(Q.$destroy(),Q=null);(b||0===b)&&k(function(b,d){e=b;Q=d;a.enter(b,null,c)})})}}}]).directive("ngAnimateChildren",["$interpolate",function(a){return{link:function(b,c,d){function f(a){c.data("$$ngAnimateChildren","on"===a||"true"===a)}var k=d.ngAnimateChildren;G(k)&&0===k.length?c.data("$$ngAnimateChildren",
|
||||
!0):(f(a(k)(b)),d.$observe("ngAnimateChildren",f))}}}]).factory("$$rAFScheduler",["$$rAF",function(a){function b(a){d=d.concat(a);c()}function c(){if(d.length){for(var b=d.shift(),e=0;e<b.length;e++)b[e]();f||a(function(){f||c()})}}var d,f;d=b.queue=[];b.waitUntilQuiet=function(b){f&&f();f=a(function(){f=null;b();c()})};return b}]).provider("$$animateQueue",["$animateProvider",function(a){function b(a){return{addClass:a.addClass,removeClass:a.removeClass,from:a.from,to:a.to}}function c(a){if(!a)return null;
|
||||
a=a.split(" ");var b=Object.create(null);s(a,function(a){b[a]=!0});return b}function d(a,b){if(a&&b){var d=c(b);return a.split(" ").some(function(a){return d[a]})}}function f(a,b,c){return e[a].some(function(a){return a(b,c)})}function k(a,b){var c=0<(a.addClass||"").length,d=0<(a.removeClass||"").length;return b?c&&d:c||d}var e=this.rules={skip:[],cancel:[],join:[]};e.join.push(function(a,b){return!a.structural&&k(a)});e.skip.push(function(a,b){return!a.structural&&!k(a)});e.skip.push(function(a,
|
||||
b){return"leave"===b.event&&a.structural});e.skip.push(function(a,b){return b.structural&&2===b.state&&!a.structural});e.cancel.push(function(a,b){return b.structural&&a.structural});e.cancel.push(function(a,b){return 2===b.state&&a.structural});e.cancel.push(function(a,b){if(b.structural)return!1;var c=a.addClass,f=a.removeClass,k=b.addClass,e=b.removeClass;return R(c)&&R(f)||R(k)&&R(e)?!1:d(c,e)||d(f,k)});this.$get=["$$rAF","$rootScope","$rootElement","$document","$$Map","$$animation","$$AnimateRunner",
|
||||
"$templateRequest","$$jqLite","$$forceReflow","$$isDocumentHidden",function(c,d,e,C,U,oa,H,u,t,I,da){function ia(a){O.delete(a.target)}function v(){var a=!1;return function(b){a?b():d.$$postDigest(function(){a=!0;b()})}}function ua(a,b,c){var g=[],l=m[c];l&&s(l,function(l){Oa.call(l.node,b)?g.push(l.callback):"leave"===c&&Oa.call(l.node,a)&&g.push(l.callback)});return g}function h(a,b,c){var l=va(b);return a.filter(function(a){return!(a.node===l&&(!c||a.callback===c))})}function q(a,J,w){function e(a,
|
||||
b,l,g){u(function(){var a=ua(ia,m,b);a.length?c(function(){s(a,function(a){a(h,l,g)});"close"!==l||m.parentNode||D.off(m)}):"close"!==l||m.parentNode||D.off(m)});a.progress(b,l,g)}function I(a){var b=h,c=n;c.preparationClasses&&(b.removeClass(c.preparationClasses),c.preparationClasses=null);c.activeClasses&&(b.removeClass(c.activeClasses),c.activeClasses=null);W(h,n);ha(h,n);n.domOperation();q.complete(!a)}var n=Da(w),h=Ha(a),m=K(h),ia=m&&m.parentNode,n=pa(n),q=new H,u=v();Z(n.addClass)&&(n.addClass=
|
||||
n.addClass.join(" "));n.addClass&&!G(n.addClass)&&(n.addClass=null);Z(n.removeClass)&&(n.removeClass=n.removeClass.join(" "));n.removeClass&&!G(n.removeClass)&&(n.removeClass=null);n.from&&!ta(n.from)&&(n.from=null);n.to&&!ta(n.to)&&(n.to=null);if(!(B&&m&&fa(m,J,w)&&Ya(m,n)))return I(),q;var x=0<=["enter","move","leave"].indexOf(J),r=da(),P=r||O.get(m);w=!P&&y.get(m)||{};var p=!!w.state;P||p&&1===w.state||(P=!E(m,ia,J));if(P)return r&&e(q,J,"start",b(n)),I(),r&&e(q,J,"close",b(n)),q;x&&F(m);r={structural:x,
|
||||
element:h,event:J,addClass:n.addClass,removeClass:n.removeClass,close:I,options:n,runner:q};if(p){if(f("skip",r,w)){if(2===w.state)return I(),q;T(h,w,r);return w.runner}if(f("cancel",r,w))if(2===w.state)w.runner.end();else if(w.structural)w.close();else return T(h,w,r),w.runner;else if(f("join",r,w))if(2===w.state)T(h,r,{});else return Ua(t,h,x?J:null,n),J=r.event=w.event,n=T(h,w,r),w.runner}else T(h,r,{});(p=r.structural)||(p="animate"===r.event&&0<Object.keys(r.options.to||{}).length||k(r));if(!p)return I(),
|
||||
g(m),q;var C=(w.counter||0)+1;r.counter=C;l(m,1,r);d.$$postDigest(function(){h=Ha(a);var c=y.get(m),d=!c,c=c||{},t=0<(h.parent()||[]).length&&("animate"===c.event||c.structural||k(c));if(d||c.counter!==C||!t){d&&(W(h,n),ha(h,n));if(d||x&&c.event!==J)n.domOperation(),q.end();t||g(m)}else J=!c.structural&&k(c,!0)?"setClass":c.event,l(m,2),c=oa(h,J,c.options),q.setHost(c),e(q,J,"start",b(n)),c.done(function(a){I(!a);(a=y.get(m))&&a.counter===C&&g(m);e(q,J,"close",b(n))})});return q}function F(a){a=a.querySelectorAll("[data-ng-animate]");
|
||||
s(a,function(a){var b=parseInt(a.getAttribute("data-ng-animate"),10),c=y.get(a);if(c)switch(b){case 2:c.runner.end();case 1:y.delete(a)}})}function g(a){a.removeAttribute("data-ng-animate");y.delete(a)}function E(a,b,c){c=C[0].body;var l=K(e),g=a===c||"HTML"===a.nodeName,d=a===l,t=!1,m=O.get(a),h;for((a=A.data(a,"$ngAnimatePin"))&&(b=K(a));b;){d||(d=b===l);if(1!==b.nodeType)break;a=y.get(b)||{};if(!t){var f=O.get(b);if(!0===f&&!1!==m){m=!0;break}else!1===f&&(m=!1);t=a.structural}if(R(h)||!0===h)a=
|
||||
A.data(b,"$$ngAnimateChildren"),za(a)&&(h=a);if(t&&!1===h)break;g||(g=b===c);if(g&&d)break;if(!d&&(a=A.data(b,"$ngAnimatePin"))){b=K(a);continue}b=b.parentNode}return(!t||h)&&!0!==m&&d&&g}function l(a,b,c){c=c||{};c.state=b;a.setAttribute("data-ng-animate",b);c=(b=y.get(a))?wa(b,c):c;y.set(a,c)}var y=new U,O=new U,B=null,P=d.$watch(function(){return 0===u.totalPendingRequests},function(a){a&&(P(),d.$$postDigest(function(){d.$$postDigest(function(){null===B&&(B=!0)})}))}),m=Object.create(null);U=a.customFilter();
|
||||
var la=a.classNameFilter();I=function(){return!0};var fa=U||I,Ya=la?function(a,b){var c=[a.getAttribute("class"),b.addClass,b.removeClass].join(" ");return la.test(c)}:I,W=aa(t),Oa=Y.Node.prototype.contains||function(a){return this===a||!!(this.compareDocumentPosition(a)&16)},D={on:function(a,b,c){var l=va(b);m[a]=m[a]||[];m[a].push({node:l,callback:c});A(b).on("$destroy",function(){y.get(l)||D.off(a,b,c)})},off:function(a,b,c){if(1!==arguments.length||G(arguments[0])){var l=m[a];l&&(m[a]=1===arguments.length?
|
||||
null:h(l,b,c))}else for(l in b=arguments[0],m)m[l]=h(m[l],b)},pin:function(a,b){Fa(sa(a),"element","not an element");Fa(sa(b),"parentElement","not an element");a.data("$ngAnimatePin",b)},push:function(a,b,c,l){c=c||{};c.domOperation=l;return q(a,b,c)},enabled:function(a,b){var c=arguments.length;if(0===c)b=!!B;else if(sa(a)){var l=K(a);if(1===c)b=!O.get(l);else{if(!O.has(l))A(a).on("$destroy",ia);O.set(l,!b)}}else b=B=!!a;return b}};return D}]}]).provider("$$animateCache",function(){var a=0,b=Object.create(null);
|
||||
this.$get=[function(){return{cacheKey:function(b,d,f,k){var e=b.parentNode;b=[e.$$ngAnimateParentKey||(e.$$ngAnimateParentKey=++a),d,b.getAttribute("class")];f&&b.push(f);k&&b.push(k);return b.join(" ")},containsCachedAnimationWithoutDuration:function(a){return(a=b[a])&&!a.isValid||!1},flush:function(){b=Object.create(null)},count:function(a){return(a=b[a])?a.total:0},get:function(a){return(a=b[a])&&a.value},put:function(a,d,f){b[a]?(b[a].total++,b[a].value=d):b[a]={total:1,value:d,isValid:f}}}}]}).provider("$$animation",
|
||||
["$animateProvider",function(a){var b=this.drivers=[];this.$get=["$$jqLite","$rootScope","$injector","$$AnimateRunner","$$Map","$$rAFScheduler","$$animateCache",function(a,d,f,k,e,Q,L){function x(a){function b(a){if(a.processed)return a;a.processed=!0;var d=a.domNode,t=d.parentNode;f.set(d,a);for(var h;t;){if(h=f.get(t)){h.processed||(h=b(h));break}t=t.parentNode}(h||c).children.push(a);return a}var c={children:[]},d,f=new e;for(d=0;d<a.length;d++){var da=a[d];f.set(da.domNode,a[d]={domNode:da.domNode,
|
||||
element:da.element,fn:da.fn,children:[]})}for(d=0;d<a.length;d++)b(a[d]);return function(a){var b=[],c=[],d;for(d=0;d<a.children.length;d++)c.push(a.children[d]);a=c.length;var t=0,f=[];for(d=0;d<c.length;d++){var g=c[d];0>=a&&(a=t,t=0,b.push(f),f=[]);f.push(g);g.children.forEach(function(a){t++;c.push(a)});a--}f.length&&b.push(f);return b}(c)}var C=[],U=aa(a);return function(e,H,u){function t(a){a=a.hasAttribute("ng-animate-ref")?[a]:a.querySelectorAll("[ng-animate-ref]");var b=[];s(a,function(a){var c=
|
||||
a.getAttribute("ng-animate-ref");c&&c.length&&b.push(a)});return b}function I(a){var b=[],c={};s(a,function(a,d){var l=K(a.element),g=0<=["enter","move"].indexOf(a.event),l=a.structural?t(l):[];if(l.length){var f=g?"to":"from";s(l,function(a){var b=a.getAttribute("ng-animate-ref");c[b]=c[b]||{};c[b][f]={animationID:d,element:A(a)}})}else b.push(a)});var d={},g={};s(c,function(c,t){var f=c.from,e=c.to;if(f&&e){var h=a[f.animationID],k=a[e.animationID],E=f.animationID.toString();if(!g[E]){var I=g[E]=
|
||||
{structural:!0,beforeStart:function(){h.beforeStart();k.beforeStart()},close:function(){h.close();k.close()},classes:da(h.classes,k.classes),from:h,to:k,anchors:[]};I.classes.length?b.push(I):(b.push(h),b.push(k))}g[E].anchors.push({out:f.element,"in":e.element})}else f=f?f.animationID:e.animationID,e=f.toString(),d[e]||(d[e]=!0,b.push(a[f]))});return b}function da(a,b){a=a.split(" ");b=b.split(" ");for(var c=[],d=0;d<a.length;d++){var g=a[d];if("ng-"!==g.substring(0,3))for(var t=0;t<b.length;t++)if(g===
|
||||
b[t]){c.push(g);break}}return c.join(" ")}function ia(a){for(var c=b.length-1;0<=c;c--){var d=f.get(b[c])(a);if(d)return d}}function v(a,b){function c(a){(a=a.data("$$animationRunner"))&&a.setHost(b)}a.from&&a.to?(c(a.from.element),c(a.to.element)):c(a.element)}function ua(){var a=e.data("$$animationRunner");!a||"leave"===H&&u.$$domOperationFired||a.end()}function h(b){e.off("$destroy",ua);e.removeData("$$animationRunner");U(e,u);ha(e,u);u.domOperation();E&&a.removeClass(e,E);F.complete(!b)}u=pa(u);
|
||||
var q=0<=["enter","move","leave"].indexOf(H),F=new k({end:function(){h()},cancel:function(){h(!0)}});if(!b.length)return h(),F;var g=Ga(e.attr("class"),Ga(u.addClass,u.removeClass)),E=u.tempClasses;E&&(g+=" "+E,u.tempClasses=null);q&&e.data("$$animatePrepareClasses","ng-"+H+"-prepare");e.data("$$animationRunner",F);C.push({element:e,classes:g,event:H,structural:q,options:u,beforeStart:function(){E=(E?E+" ":"")+"ng-animate";a.addClass(e,E);var b=e.data("$$animatePrepareClasses");b&&a.removeClass(e,
|
||||
b)},close:h});e.on("$destroy",ua);if(1<C.length)return F;d.$$postDigest(function(){var b=[];s(C,function(a){a.element.data("$$animationRunner")?b.push(a):a.close()});C.length=0;var d=I(b),g=[];s(d,function(a){var b=a.from?a.from.element:a.element,c=u.addClass,d=L.cacheKey(b[0],a.event,(c?c+" ":"")+"ng-animate",u.removeClass);g.push({element:b,domNode:K(b),fn:function(){var b,c=a.close;if(L.containsCachedAnimationWithoutDuration(d))c();else{a.beforeStart();if((a.anchors?a.from.element||a.to.element:
|
||||
a.element).data("$$animationRunner")){var g=ia(a);g&&(b=g.start)}b?(b=b(),b.done(function(a){c(!a)}),v(a,b)):c()}}})});for(var d=x(g),t=0;t<d.length;t++)for(var f=d[t],e=0;e<f.length;e++){var h=f[e],k=h.element;d[t][e]=h.fn;0===t?k.removeData("$$animatePrepareClasses"):(h=k.data("$$animatePrepareClasses"))&&a.addClass(k,h)}Q(d)});return F}}]}]).provider("$animateCss",["$animateProvider",function(a){this.$get=["$window","$$jqLite","$$AnimateRunner","$timeout","$$animateCache","$$forceReflow","$sniffer",
|
||||
"$$rAFScheduler","$$animateQueue",function(a,c,d,f,k,e,Q,L,x){function C(d,f,e,x){var v,s="stagger-"+e;0<k.count(e)&&(v=k.get(s),v||(f=$(f,"-stagger"),c.addClass(d,f),v=Ka(a,d,x),v.animationDuration=Math.max(v.animationDuration,0),v.transitionDuration=Math.max(v.transitionDuration,0),c.removeClass(d,f),k.put(s,v,!0)));return v||{}}function U(a){u.push(a);L.waitUntilQuiet(function(){k.flush();for(var a=e(),b=0;b<u.length;b++)u[b](a);u.length=0})}function z(c,d,f,e){d=k.get(f);d||(d=Ka(a,c,Wa),"infinite"===
|
||||
d.animationIterationCount&&(d.animationIterationCount=1));k.put(f,d,e||0<d.transitionDuration||0<d.animationDuration);c=d;f=c.animationDelay;e=c.transitionDelay;c.maxDelay=f&&e?Math.max(f,e):f||e;c.maxDuration=Math.max(c.animationDuration*c.animationIterationCount,c.transitionDuration);return c}var H=aa(c),u=[];return function(a,b){function e(){v()}function L(){v(!0)}function v(b){if(!(P||la&&m)){P=!0;m=!1;V&&!g.$$skipPreparationClasses&&c.removeClass(a,V);ba&&c.removeClass(a,ba);xa(l,!1);qa(l,!1);
|
||||
s(y,function(a){l.style[a[0]]=""});H(a,g);ha(a,g);Object.keys(E).length&&s(E,function(a,b){a?l.style.setProperty(b,a):l.style.removeProperty(b)});if(g.onDone)g.onDone();w&&w.length&&a.off(w.join(" "),q);var d=a.data("$$animateCss");d&&(f.cancel(d[0].timer),a.removeData("$$animateCss"));fa&&fa.complete(!b)}}function u(a){p.blockTransition&&qa(l,a);p.blockKeyframeAnimation&&xa(l,!!a)}function h(){fa=new d({end:e,cancel:L});U(N);v();return{$$willAnimate:!1,start:function(){return fa},end:e}}function q(a){a.stopPropagation();
|
||||
var b=a.originalEvent||a;b.target===l&&(a=b.$manualTimeStamp||Date.now(),b=parseFloat(b.elapsedTime.toFixed(3)),Math.max(a-J,0)>=G&&b>=D&&(la=!0,v()))}function F(){function b(){if(!P){u(!1);s(y,function(a){l.style[a[0]]=a[1]});H(a,g);c.addClass(a,ba);if(p.recalculateTimingStyles){T=l.getAttribute("class")+" "+V;ka=k.cacheKey(l,ja,g.addClass,g.removeClass);r=z(l,T,ka,!1);ga=r.maxDelay;W=Math.max(ga,0);D=r.maxDuration;if(0===D){v();return}p.hasTransitions=0<r.transitionDuration;p.hasAnimations=0<r.animationDuration}p.applyAnimationDelay&&
|
||||
(ga="boolean"!==typeof g.delay&&ya(g.delay)?parseFloat(g.delay):ga,W=Math.max(ga,0),r.animationDelay=ga,ea=[ra,ga+"s"],y.push(ea),l.style[ea[0]]=ea[1]);G=1E3*W;R=1E3*D;if(g.easing){var e,h=g.easing;p.hasTransitions&&(e=M+"TimingFunction",y.push([e,h]),l.style[e]=h);p.hasAnimations&&(e=ca+"TimingFunction",y.push([e,h]),l.style[e]=h)}r.transitionDuration&&w.push(Aa);r.animationDuration&&w.push(Ba);J=Date.now();var m=G+1.5*R;e=J+m;var h=a.data("$$animateCss")||[],F=!0;if(h.length){var n=h[0];(F=e>n.expectedEndTime)?
|
||||
f.cancel(n.timer):h.push(v)}F&&(m=f(d,m,!1),h[0]={timer:m,expectedEndTime:e},h.push(v),a.data("$$animateCss",h));if(w.length)a.on(w.join(" "),q);g.to&&(g.cleanupStyles&&Ma(E,l,Object.keys(g.to)),Ja(a,g))}}function d(){var b=a.data("$$animateCss");if(b){for(var c=1;c<b.length;c++)b[c]();a.removeData("$$animateCss")}}if(!P)if(l.parentNode){var e=function(a){if(la)m&&a&&(m=!1,v());else if(m=!a,r.animationDuration)if(a=xa(l,m),m)y.push(a);else{var b=y,c=b.indexOf(a);0<=a&&b.splice(c,1)}},h=0<aa&&(r.transitionDuration&&
|
||||
0===X.transitionDuration||r.animationDuration&&0===X.animationDuration)&&Math.max(X.animationDelay,X.transitionDelay);h?f(b,Math.floor(h*aa*1E3),!1):b();A.resume=function(){e(!0)};A.pause=function(){e(!1)}}else v()}var g=b||{};g.$$prepared||(g=pa(Da(g)));var E={},l=K(a);if(!l||!l.parentNode||!x.enabled())return h();var y=[],O=a.attr("class"),B=Qa(g),P,m,la,fa,A,W,G,D,R,J,w=[];if(0===g.duration||!Q.animations&&!Q.transitions)return h();var ja=g.event&&Z(g.event)?g.event.join(" "):g.event,Y=ja&&g.structural,
|
||||
n="",S="";Y?n=$(ja,"ng-",!0):ja&&(n=ja);g.addClass&&(S+=$(g.addClass,"-add"));g.removeClass&&(S.length&&(S+=" "),S+=$(g.removeClass,"-remove"));g.applyClassesEarly&&S.length&&H(a,g);var V=[n,S].join(" ").trim(),T=O+" "+V,O=B.to&&0<Object.keys(B.to).length;if(!(0<(g.keyframeStyle||"").length||O||V))return h();var X,ka=k.cacheKey(l,ja,g.addClass,g.removeClass);if(k.containsCachedAnimationWithoutDuration(ka))return V=null,h();0<g.stagger?(B=parseFloat(g.stagger),X={transitionDelay:B,animationDelay:B,
|
||||
transitionDuration:0,animationDuration:0}):X=C(l,V,ka,Xa);g.$$skipPreparationClasses||c.addClass(a,V);g.transitionStyle&&(B=[M,g.transitionStyle],ma(l,B),y.push(B));0<=g.duration&&(B=0<l.style[M].length,B=La(g.duration,B),ma(l,B),y.push(B));g.keyframeStyle&&(B=[ca,g.keyframeStyle],ma(l,B),y.push(B));var aa=X?0<=g.staggerIndex?g.staggerIndex:k.count(ka):0;(n=0===aa)&&!g.skipBlocking&&qa(l,9999);var r=z(l,T,ka,!Y),ga=r.maxDelay;W=Math.max(ga,0);D=r.maxDuration;var p={};p.hasTransitions=0<r.transitionDuration;
|
||||
p.hasAnimations=0<r.animationDuration;p.hasTransitionAll=p.hasTransitions&&"all"===r.transitionProperty;p.applyTransitionDuration=O&&(p.hasTransitions&&!p.hasTransitionAll||p.hasAnimations&&!p.hasTransitions);p.applyAnimationDuration=g.duration&&p.hasAnimations;p.applyTransitionDelay=ya(g.delay)&&(p.applyTransitionDuration||p.hasTransitions);p.applyAnimationDelay=ya(g.delay)&&p.hasAnimations;p.recalculateTimingStyles=0<S.length;if(p.applyTransitionDuration||p.applyAnimationDuration)D=g.duration?parseFloat(g.duration):
|
||||
D,p.applyTransitionDuration&&(p.hasTransitions=!0,r.transitionDuration=D,B=0<l.style[M+"Property"].length,y.push(La(D,B))),p.applyAnimationDuration&&(p.hasAnimations=!0,r.animationDuration=D,y.push([Ca,D+"s"]));if(0===D&&!p.recalculateTimingStyles)return h();var ba=$(V,"-active");if(null!=g.delay){var ea;"boolean"!==typeof g.delay&&(ea=parseFloat(g.delay),W=Math.max(ea,0));p.applyTransitionDelay&&y.push([na,ea+"s"]);p.applyAnimationDelay&&y.push([ra,ea+"s"])}null==g.duration&&0<r.transitionDuration&&
|
||||
(p.recalculateTimingStyles=p.recalculateTimingStyles||n);G=1E3*W;R=1E3*D;g.skipBlocking||(p.blockTransition=0<r.transitionDuration,p.blockKeyframeAnimation=0<r.animationDuration&&0<X.animationDelay&&0===X.animationDuration);g.from&&(g.cleanupStyles&&Ma(E,l,Object.keys(g.from)),Ia(a,g));p.blockTransition||p.blockKeyframeAnimation?u(D):g.skipBlocking||qa(l,!1);return{$$willAnimate:!0,end:e,start:function(){if(!P)return A={end:e,cancel:L,resume:null,pause:null},fa=new d(A),U(F),fa}}}}]}]).provider("$$animateCssDriver",
|
||||
["$$animationProvider",function(a){a.drivers.push("$$animateCssDriver");this.$get=["$animateCss","$rootScope","$$AnimateRunner","$rootElement","$sniffer","$$jqLite","$document",function(a,c,d,f,k,e,Q){function L(a){return a.replace(/\bng-\S+\b/g,"")}function x(a,b){G(a)&&(a=a.split(" "));G(b)&&(b=b.split(" "));return a.filter(function(a){return-1===b.indexOf(a)}).join(" ")}function C(c,e,f){function k(a){var b={},c=K(a).getBoundingClientRect();s(["width","height","top","left"],function(a){var d=c[a];
|
||||
switch(a){case "top":d+=H.scrollTop;break;case "left":d+=H.scrollLeft}b[a]=Math.floor(d)+"px"});return b}function v(){var c=L(f.attr("class")||""),d=x(c,q),c=x(q,c),d=a(h,{to:k(f),addClass:"ng-anchor-in "+d,removeClass:"ng-anchor-out "+c,delay:!0});return d.$$willAnimate?d:null}function C(){h.remove();e.removeClass("ng-animate-shim");f.removeClass("ng-animate-shim")}var h=A(K(e).cloneNode(!0)),q=L(h.attr("class")||"");e.addClass("ng-animate-shim");f.addClass("ng-animate-shim");h.addClass("ng-anchor");
|
||||
u.append(h);var F;c=function(){var c=a(h,{addClass:"ng-anchor-out",delay:!0,from:k(e)});return c.$$willAnimate?c:null}();if(!c&&(F=v(),!F))return C();var g=c||F;return{start:function(){function a(){c&&c.end()}var b,c=g.start();c.done(function(){c=null;if(!F&&(F=v()))return c=F.start(),c.done(function(){c=null;C();b.complete()}),c;C();b.complete()});return b=new d({end:a,cancel:a})}}}function z(a,b,c,e){var f=oa(a,N),k=oa(b,N),h=[];s(e,function(a){(a=C(c,a.out,a["in"]))&&h.push(a)});if(f||k||0!==h.length)return{start:function(){function a(){s(b,
|
||||
function(a){a.end()})}var b=[];f&&b.push(f.start());k&&b.push(k.start());s(h,function(a){b.push(a.start())});var c=new d({end:a,cancel:a});d.all(b,function(a){c.complete(a)});return c}}}function oa(c){var d=c.element,e=c.options||{};c.structural&&(e.event=c.event,e.structural=!0,e.applyClassesEarly=!0,"leave"===c.event&&(e.onDone=e.domOperation));e.preparationClasses&&(e.event=ba(e.event,e.preparationClasses));c=a(d,e);return c.$$willAnimate?c:null}if(!k.animations&&!k.transitions)return N;var H=
|
||||
Q[0].body;c=K(f);var u=A(c.parentNode&&11===c.parentNode.nodeType||H.contains(c)?c:H);return function(a){return a.from&&a.to?z(a.from,a.to,a.classes,a.anchors):oa(a)}}]}]).provider("$$animateJs",["$animateProvider",function(a){this.$get=["$injector","$$AnimateRunner","$$jqLite",function(b,c,d){function f(c){c=Z(c)?c:c.split(" ");for(var d=[],f={},k=0;k<c.length;k++){var s=c[k],z=a.$$registeredAnimations[s];z&&!f[s]&&(d.push(b.get(z)),f[s]=!0)}return d}var k=aa(d);return function(a,b,d,x){function C(){x.domOperation();
|
||||
k(a,x)}function z(a,b,d,f,e){switch(d){case "animate":b=[b,f.from,f.to,e];break;case "setClass":b=[b,t,I,e];break;case "addClass":b=[b,t,e];break;case "removeClass":b=[b,I,e];break;default:b=[b,e]}b.push(f);if(a=a.apply(a,b))if(Ea(a.start)&&(a=a.start()),a instanceof c)a.done(e);else if(Ea(a))return a;return N}function A(a,b,d,e,f){var h=[];s(e,function(e){var l=e[f];l&&h.push(function(){var e,f,h=!1,k=function(a){h||(h=!0,(f||N)(a),e.complete(!a))};e=new c({end:function(){k()},cancel:function(){k(!0)}});
|
||||
f=z(l,a,b,d,function(a){k(!1===a)});return e})});return h}function H(a,b,d,e,f){var h=A(a,b,d,e,f);if(0===h.length){var k,q;"beforeSetClass"===f?(k=A(a,"removeClass",d,e,"beforeRemoveClass"),q=A(a,"addClass",d,e,"beforeAddClass")):"setClass"===f&&(k=A(a,"removeClass",d,e,"removeClass"),q=A(a,"addClass",d,e,"addClass"));k&&(h=h.concat(k));q&&(h=h.concat(q))}if(0!==h.length)return function(a){var b=[];h.length&&s(h,function(a){b.push(a())});b.length?c.all(b,a):a();return function(a){s(b,function(b){a?
|
||||
b.cancel():b.end()})}}}var u=!1;3===arguments.length&&ta(d)&&(x=d,d=null);x=pa(x);d||(d=a.attr("class")||"",x.addClass&&(d+=" "+x.addClass),x.removeClass&&(d+=" "+x.removeClass));var t=x.addClass,I=x.removeClass,G=f(d),K,v;if(G.length){var M,h;"leave"===b?(h="leave",M="afterLeave"):(h="before"+b.charAt(0).toUpperCase()+b.substr(1),M=b);"enter"!==b&&"move"!==b&&(K=H(a,b,x,G,h));v=H(a,b,x,G,M)}if(K||v){var q;return{$$willAnimate:!0,end:function(){q?q.end():(u=!0,C(),ha(a,x),q=new c,q.complete(!0));
|
||||
return q},start:function(){function b(c){u=!0;C();ha(a,x);q.complete(c)}if(q)return q;q=new c;var d,f=[];K&&f.push(function(a){d=K(a)});f.length?f.push(function(a){C();a(!0)}):C();v&&f.push(function(a){d=v(a)});q.setHost({end:function(){u||((d||N)(void 0),b(void 0))},cancel:function(){u||((d||N)(!0),b(!0))}});c.chain(f,b);return q}}}}}]}]).provider("$$animateJsDriver",["$$animationProvider",function(a){a.drivers.push("$$animateJsDriver");this.$get=["$$animateJs","$$AnimateRunner",function(a,c){function d(c){return a(c.element,
|
||||
c.event,c.classes,c.options)}return function(a){if(a.from&&a.to){var b=d(a.from),e=d(a.to);if(b||e)return{start:function(){function a(){return function(){s(d,function(a){a.end()})}}var d=[];b&&d.push(b.start());e&&d.push(e.start());c.all(d,function(a){f.complete(a)});var f=new c({end:a(),cancel:a()});return f}}}else return d(a)}}]}])})(window,window.angular);
|
||||
//# sourceMappingURL=angular-animate.min.js.map
|
17
playmaker/playmaker/static/js/angular-route.min.js
vendored
Normal file
17
playmaker/playmaker/static/js/angular-route.min.js
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
AngularJS v1.7.5
|
||||
(c) 2010-2018 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(I,b){'use strict';function z(b,h){var d=[],c=b.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)(\*\?|[?*])?/g,function(b,c,h,k){b="?"===k||"*?"===k;k="*"===k||"*?"===k;d.push({name:h,optional:b});c=c||"";return(b?"(?:"+c:c+"(?:")+(k?"(.+?)":"([^/]+)")+(b?"?)?":")")}).replace(/([/$*])/g,"\\$1");h.ignoreTrailingSlashes&&(c=c.replace(/\/+$/,"")+"/*");return{keys:d,regexp:new RegExp("^"+c+"(?:[?#]|$)",h.caseInsensitiveMatch?"i":"")}}function A(b){p&&b.get("$route")}function v(u,h,d){return{restrict:"ECA",
|
||||
terminal:!0,priority:400,transclude:"element",link:function(c,f,g,l,k){function q(){r&&(d.cancel(r),r=null);m&&(m.$destroy(),m=null);s&&(r=d.leave(s),r.done(function(b){!1!==b&&(r=null)}),s=null)}function C(){var g=u.current&&u.current.locals;if(b.isDefined(g&&g.$template)){var g=c.$new(),l=u.current;s=k(g,function(g){d.enter(g,null,s||f).done(function(d){!1===d||!b.isDefined(w)||w&&!c.$eval(w)||h()});q()});m=l.scope=g;m.$emit("$viewContentLoaded");m.$eval(p)}else q()}var m,s,r,w=g.autoscroll,p=g.onload||
|
||||
"";c.$on("$routeChangeSuccess",C);C()}}}function x(b,h,d){return{restrict:"ECA",priority:-400,link:function(c,f){var g=d.current,l=g.locals;f.html(l.$template);var k=b(f.contents());if(g.controller){l.$scope=c;var q=h(g.controller,l);g.controllerAs&&(c[g.controllerAs]=q);f.data("$ngControllerController",q);f.children().data("$ngControllerController",q)}c[g.resolveAs||"$resolve"]=l;k(c)}}}var D,E,F,G,y=b.module("ngRoute",[]).info({angularVersion:"1.7.5"}).provider("$route",function(){function u(d,
|
||||
c){return b.extend(Object.create(d),c)}D=b.isArray;E=b.isObject;F=b.isDefined;G=b.noop;var h={};this.when=function(d,c){var f;f=void 0;if(D(c)){f=f||[];for(var g=0,l=c.length;g<l;g++)f[g]=c[g]}else if(E(c))for(g in f=f||{},c)if("$"!==g.charAt(0)||"$"!==g.charAt(1))f[g]=c[g];f=f||c;b.isUndefined(f.reloadOnUrl)&&(f.reloadOnUrl=!0);b.isUndefined(f.reloadOnSearch)&&(f.reloadOnSearch=!0);b.isUndefined(f.caseInsensitiveMatch)&&(f.caseInsensitiveMatch=this.caseInsensitiveMatch);h[d]=b.extend(f,{originalPath:d},
|
||||
d&&z(d,f));d&&(g="/"===d[d.length-1]?d.substr(0,d.length-1):d+"/",h[g]=b.extend({originalPath:d,redirectTo:d},z(g,f)));return this};this.caseInsensitiveMatch=!1;this.otherwise=function(b){"string"===typeof b&&(b={redirectTo:b});this.when(null,b);return this};p=!0;this.eagerInstantiationEnabled=function(b){return F(b)?(p=b,this):p};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce","$browser",function(d,c,f,g,l,k,q,p){function m(a){var e=t.current;n=A();(x=
|
||||
!B&&n&&e&&n.$$route===e.$$route&&(!n.reloadOnUrl||!n.reloadOnSearch&&b.equals(n.pathParams,e.pathParams)))||!e&&!n||d.$broadcast("$routeChangeStart",n,e).defaultPrevented&&a&&a.preventDefault()}function s(){var a=t.current,e=n;if(x)a.params=e.params,b.copy(a.params,f),d.$broadcast("$routeUpdate",a);else if(e||a){B=!1;t.current=e;var c=g.resolve(e);p.$$incOutstandingRequestCount("$route");c.then(r).then(w).then(function(g){return g&&c.then(y).then(function(c){e===t.current&&(e&&(e.locals=c,b.copy(e.params,
|
||||
f)),d.$broadcast("$routeChangeSuccess",e,a))})}).catch(function(b){e===t.current&&d.$broadcast("$routeChangeError",e,a,b)}).finally(function(){p.$$completeOutstandingRequest(G,"$route")})}}function r(a){var e={route:a,hasRedirection:!1};if(a)if(a.redirectTo)if(b.isString(a.redirectTo))e.path=v(a.redirectTo,a.params),e.search=a.params,e.hasRedirection=!0;else{var d=c.path(),f=c.search();a=a.redirectTo(a.pathParams,d,f);b.isDefined(a)&&(e.url=a,e.hasRedirection=!0)}else if(a.resolveRedirectTo)return g.resolve(l.invoke(a.resolveRedirectTo)).then(function(a){b.isDefined(a)&&
|
||||
(e.url=a,e.hasRedirection=!0);return e});return e}function w(a){var b=!0;if(a.route!==t.current)b=!1;else if(a.hasRedirection){var g=c.url(),d=a.url;d?c.url(d).replace():d=c.path(a.path).search(a.search).replace().url();d!==g&&(b=!1)}return b}function y(a){if(a){var e=b.extend({},a.resolve);b.forEach(e,function(a,c){e[c]=b.isString(a)?l.get(a):l.invoke(a,null,null,c)});a=z(a);b.isDefined(a)&&(e.$template=a);return g.all(e)}}function z(a){var e,c;b.isDefined(e=a.template)?b.isFunction(e)&&(e=e(a.params)):
|
||||
b.isDefined(c=a.templateUrl)&&(b.isFunction(c)&&(c=c(a.params)),b.isDefined(c)&&(a.loadedTemplateUrl=q.valueOf(c),e=k(c)));return e}function A(){var a,e;b.forEach(h,function(d,g){var f;if(f=!e){var h=c.path();f=d.keys;var l={};if(d.regexp)if(h=d.regexp.exec(h)){for(var k=1,p=h.length;k<p;++k){var m=f[k-1],n=h[k];m&&n&&(l[m.name]=n)}f=l}else f=null;else f=null;f=a=f}f&&(e=u(d,{params:b.extend({},c.search(),a),pathParams:a}),e.$$route=d)});return e||h[null]&&u(h[null],{params:{},pathParams:{}})}function v(a,
|
||||
c){var d=[];b.forEach((a||"").split(":"),function(a,b){if(0===b)d.push(a);else{var f=a.match(/(\w+)(?:[?*])?(.*)/),g=f[1];d.push(c[g]);d.push(f[2]||"");delete c[g]}});return d.join("")}var B=!1,n,x,t={routes:h,reload:function(){B=!0;var a={defaultPrevented:!1,preventDefault:function(){this.defaultPrevented=!0;B=!1}};d.$evalAsync(function(){m(a);a.defaultPrevented||s()})},updateParams:function(a){if(this.current&&this.current.$$route)a=b.extend({},this.current.params,a),c.path(v(this.current.$$route.originalPath,
|
||||
a)),c.search(a);else throw H("norout");}};d.$on("$locationChangeStart",m);d.$on("$locationChangeSuccess",s);return t}]}).run(A),H=b.$$minErr("ngRoute"),p;A.$inject=["$injector"];y.provider("$routeParams",function(){this.$get=function(){return{}}});y.directive("ngView",v);y.directive("ngView",x);v.$inject=["$route","$anchorScroll","$animate"];x.$inject=["$compile","$controller","$route"]})(window,window.angular);
|
||||
//# sourceMappingURL=angular-route.min.js.map
|
10
playmaker/playmaker/static/js/angular-touch.min.js
vendored
Normal file
10
playmaker/playmaker/static/js/angular-touch.min.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/*
|
||||
AngularJS v1.7.5
|
||||
(c) 2010-2018 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(t,p){'use strict';function q(g,h,s){n.directive(g,["$parse","$swipe",function(a,b){return function(c,e,f){function k(a){if(!d)return!1;var b=Math.abs(a.y-d.y);a=(a.x-d.x)*h;return l&&75>b&&0<a&&30<a&&.3>b/a}var m=a(f[g]),d,l,r=["touch"];p.isDefined(f.ngSwipeDisableMouse)||r.push("mouse");b.bind(e,{start:function(a,b){d=a;l=!0},cancel:function(a){l=!1},end:function(a,b){k(a)&&c.$apply(function(){e.triggerHandler(s);m(c,{$event:b})})}},r)}}])}var n=p.module("ngTouch",[]);n.info({angularVersion:"1.7.5"});
|
||||
n.factory("$swipe",[function(){function g(a){a=a.originalEvent||a;var b=a.touches&&a.touches.length?a.touches:[a];a=a.changedTouches&&a.changedTouches[0]||b[0];return{x:a.clientX,y:a.clientY}}function h(a,b){var c=[];p.forEach(a,function(a){(a=n[a][b])&&c.push(a)});return c.join(" ")}var n={mouse:{start:"mousedown",move:"mousemove",end:"mouseup"},touch:{start:"touchstart",move:"touchmove",end:"touchend",cancel:"touchcancel"},pointer:{start:"pointerdown",move:"pointermove",end:"pointerup",cancel:"pointercancel"}};
|
||||
return{bind:function(a,b,c){var e,f,k,m,d=!1;c=c||["mouse","touch","pointer"];a.on(h(c,"start"),function(a){k=g(a);d=!0;f=e=0;m=k;b.start&&b.start(k,a)});var l=h(c,"cancel");if(l)a.on(l,function(a){d=!1;b.cancel&&b.cancel(a)});a.on(h(c,"move"),function(a){if(d&&k){var c=g(a);e+=Math.abs(c.x-m.x);f+=Math.abs(c.y-m.y);m=c;10>e&&10>f||(f>e?(d=!1,b.cancel&&b.cancel(a)):(a.preventDefault(),b.move&&b.move(c,a)))}});a.on(h(c,"end"),function(a){d&&(d=!1,b.end&&b.end(g(a),a))})}}}]);q("ngSwipeLeft",-1,"swipeleft");
|
||||
q("ngSwipeRight",1,"swiperight")})(window,window.angular);
|
||||
//# sourceMappingURL=angular-touch.min.js.map
|
5988
playmaker/playmaker/static/js/crypto-js.js
Normal file
5988
playmaker/playmaker/static/js/crypto-js.js
Normal file
File diff suppressed because it is too large
Load Diff
10
playmaker/playmaker/static/js/ui-bootstrap.min.js
vendored
Normal file
10
playmaker/playmaker/static/js/ui-bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
61
playmaker/playmaker/views/app.html
Normal file
61
playmaker/playmaker/views/app.html
Normal file
@ -0,0 +1,61 @@
|
||||
<div>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-list">
|
||||
<div class="sidebar-list-el">
|
||||
Update applications <a ng-click="$ctrl.check()" class=
|
||||
"btn btn-default sidebar-button">
|
||||
<i class="fas fa-sync-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-list-el">
|
||||
Update fdroid repo <a ng-click="$ctrl.fdroid()" class=
|
||||
"btn btn-default">
|
||||
<i class="fab fa-android" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-list-el">
|
||||
URL: <code>{{$ctrl.baseUrl}}/fdroid</code>
|
||||
</div>
|
||||
<div class="sidebar-list-el">
|
||||
<strong>Last update:</strong> {{$ctrl.lastFdroidUpdate}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-container">
|
||||
<div ng-repeat="app in $ctrl.apps" class="col-xs-12 col-md-12 col-lg-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<img ng-if="$ctrl.desktop" width="100" height="100" ng-src=
|
||||
"{{app.previewImage[0].imageUrl}}">
|
||||
<div class="apk-info">
|
||||
<strong><span class=
|
||||
"apk-item-title">{{app.title}}</span></strong>
|
||||
<i ng-repeat="val in app.starList" ng-class=
|
||||
"val.full ? 'fas fa-star' : 'far fa-star'"
|
||||
aria-hidden="true"></i>
|
||||
<strong>{{app.formattedStars}}</strong><br>
|
||||
<span><strong>Developer:</strong> {{app.creator}}</span><br>
|
||||
<span><strong>Version:</strong> {{app.details.appDetails.versionCode}}</span><br>
|
||||
<span><strong>Files:</strong> {{app.details.appDetails.file.length}}</span><br>
|
||||
<span ng-if="app.formattedSize !== undefined"><strong>Size:</strong> {{app.formattedSize}} MB</span>
|
||||
<span ng-if="app.formattedSize === undefined"><strong>Size:</strong> unknown</span><br>
|
||||
<span><strong>PackageId:</strong> {{app.docid}}</span><br>
|
||||
</div>
|
||||
</div><!-- panel-body -->
|
||||
<div ng-hide="!app.updating" class="apk-progress">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-striped active" role=
|
||||
"progressbar" aria-valuenow="100" aria-valuemin="0"
|
||||
aria-valuemax="100" style="width:100%">
|
||||
<span class="sr-only">Updating</span>
|
||||
</div><!-- progress-bar -->
|
||||
</div><!-- progress -->
|
||||
</div><!-- apk-progress -->
|
||||
<div ng-hide="app.updating" class="apk-buttons">
|
||||
<a ng-click="$ctrl.delete(app)" class=
|
||||
"btn btn-raised btn-danger"> delete</a>
|
||||
</div>
|
||||
</div><!-- panel -->
|
||||
</div><!-- main -->
|
||||
</div>
|
||||
</div>
|
42
playmaker/playmaker/views/login.html
Normal file
42
playmaker/playmaker/views/login.html
Normal file
@ -0,0 +1,42 @@
|
||||
<div class="view-container">
|
||||
<form>
|
||||
<div class="login-body">
|
||||
<h2>First time initialization</h2>
|
||||
<p>In order to initalize the server, you need to enter your google
|
||||
credentials. They are sent to the server, which will perform the login
|
||||
procedure and fetch an authorization token, and then they are
|
||||
discarded. To secure communucation between client and server you should
|
||||
configure playmaker with https, like explained <a href=
|
||||
"https://github.com/NoMore201/playmaker#usage">here</a></p>
|
||||
<div class="form-group">
|
||||
<label for="emailInput">Email address</label> <input type="text"
|
||||
class="form-control" id="emailInput" placeholder="Email" ng-model=
|
||||
"user.email" uib-tooltip=
|
||||
"Enter a valid username (with or without '@gmail.com')"
|
||||
tooltip-enable="$ctrl.badUsername" tooltip-placement="top"
|
||||
tooltip-is-open="$ctrl.badUsername" ng-disabled="$ctrl.loggingIn">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="passwordInput">Password</label> <input type="password"
|
||||
class="form-control" id="passwordInput" placeholder="Password"
|
||||
ng-model="user.password" uib-tooltip="Enter a valid password"
|
||||
tooltip-enable="$ctrl.badPassword" tooltip-placement="bottom"
|
||||
tooltip-is-open="$ctrl.badPassword" ng-disabled="$ctrl.loggingIn">
|
||||
</div><button type="submit" class="btn btn-default" ng-click=
|
||||
"$ctrl.login(user)" ng-disabled="$ctrl.loggingIn">Submit</button>
|
||||
<a class="btn btn-warning"
|
||||
href="https://accounts.google.com/b/0/DisplayUnlockCaptcha"
|
||||
target="_blank"
|
||||
ng-disabled="!$ctrl.securityCheck">Security check</a>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-hide="!$ctrl.loggingIn" class="row loading-login">
|
||||
<div class="loading-login-text">
|
||||
Loading apps into cache, please wait
|
||||
</div>
|
||||
<uib-progressbar class="progress-striped active" max="$ctrl.max" value=
|
||||
"$ctrl.current"><span style=
|
||||
"color:white; white-space:nowrap;">{{$ctrl.formattedPercent}}%</span>
|
||||
</uib-progressbar>
|
||||
</div>
|
||||
</div>
|
101
playmaker/playmaker/views/search.html
Normal file
101
playmaker/playmaker/views/search.html
Normal file
@ -0,0 +1,101 @@
|
||||
<div class="view-container">
|
||||
<div id="search-area" class="input-group input-group-lg">
|
||||
<span class="input-group-addon" id="search-input-label">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
<input ng-model="searchString" on-enter="$ctrl.search(searchString)"
|
||||
type="text" class="form-control" id="search-input" placeholder="Search"
|
||||
aria-describedby="search-input-label">
|
||||
</div>
|
||||
<div ng-hide="!$ctrl.searching" id="search-progress" class="progress">
|
||||
<div class="progress-bar progress-bar-striped active" role="progressbar"
|
||||
aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width:100%">
|
||||
<span class="sr-only">Loading results</span>
|
||||
</div>
|
||||
</div><!-- progress -->
|
||||
<div ng-if="$ctrl.desktop" class="row" id="table-box">
|
||||
<table class="table" ng-hide="$ctrl.results.length === 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Title</th>
|
||||
<th>Developer</th>
|
||||
<th>Id</th>
|
||||
<th>Version</th>
|
||||
<th>Size</th>
|
||||
<th>Stars</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body">
|
||||
<tr ng-repeat="app in $ctrl.results">
|
||||
<td id="dl-button-td">
|
||||
<a ng-click="$ctrl.download(app)" ng-class=
|
||||
"{'dl-button-disabled': app.disabled, 'dl-button': !app.disabled}">
|
||||
<div ng-hide="app.downloading" class="fa-2x">
|
||||
<i class="fas fa-download"></i>
|
||||
</div>
|
||||
<div ng-hide="!app.downloading" class="fa-2x">
|
||||
<i class="fas fa-cog fa-spin"></i>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{app.title}}</td>
|
||||
<td>{{app.creator}}</td>
|
||||
<td>{{app.docid}}</td>
|
||||
<td>{{app.details.appDetails.versionCode}}</td>
|
||||
<td>{{ ((app.details.appDetails.file[0].size)/(1024*1024)).toFixed(2) }} MB</td>
|
||||
<td>{{app.aggregateRating.starRating.toFixed(2)}} Stars</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div ng-if="$ctrl.mobile" class="row" id="table-box">
|
||||
<script type="text/ng-template" id="myModalContent.html">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="modal-title">{{app.title}}</h3>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-body">
|
||||
<strong>Id:</strong> {{app.docid}}<br>
|
||||
<strong>Developer:</strong> {{app.creator}}<br>
|
||||
<strong>Version:</strong> {{app.details.appDetails.versionCode}}<br>
|
||||
<strong>Size:</strong> {{ ((app.details.appDetails.file[0].size)/(1024*1024)).toFixed(2) }} MB<br>
|
||||
<strong>Stars:</strong> {{app.aggregateRating.starRating.toFixed(2)}}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-default" type="button" ng-click="$close()">Close</button>
|
||||
</div>
|
||||
</script>
|
||||
<table class="table" ng-hide="$ctrl.results.length === 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Title</th>
|
||||
<th>Info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body">
|
||||
<tr ng-repeat="app in $ctrl.results">
|
||||
<td id="dl-button-td">
|
||||
<a ng-click="$ctrl.download(app)" ng-class=
|
||||
"{'dl-button-disabled': app.disabled, 'dl-button': !app.disabled}">
|
||||
<div ng-hide="app.downloading" class="fa-2x">
|
||||
<i class="fas fa-download"></i>
|
||||
</div>
|
||||
<div ng-hide="!app.downloading" class="fa-2x">
|
||||
<i class="fas fa-cog fa-spin"></i>
|
||||
</div>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{app.title}}</td>
|
||||
<td>
|
||||
<a ng-click="$ctrl.modalOpen(app)" class="dl-button">
|
||||
<i class="fas fa-info-circle fa-2x" data-toggle="popover"
|
||||
data-html="true" title="" data-placement="left"
|
||||
data-content="" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div><!-- row -->
|
||||
</div><!-- container -->
|
70
playmaker/pm-server
Normal file
70
playmaker/pm-server
Normal file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
from tornado import httpserver
|
||||
from tornado import ioloop as io
|
||||
import functools
|
||||
import tornado_crontab
|
||||
import os
|
||||
import configparser
|
||||
|
||||
from playmaker.server import createServer
|
||||
from playmaker.service import Play
|
||||
|
||||
|
||||
def auto_update(service):
|
||||
if service.loggedIn:
|
||||
print('Executing auto update cron task')
|
||||
apps = service.check_local_apks().get('message')
|
||||
if len(apps) > 0:
|
||||
service.download_selection(apps)
|
||||
service.fdroid_update()
|
||||
|
||||
|
||||
# tornado setup
|
||||
if __name__ == '__main__':
|
||||
# arguments parsing
|
||||
ap = argparse.ArgumentParser(description='Apk and fdroid repository ' +
|
||||
'manager with a web interface.')
|
||||
ap.add_argument('-f', '--fdroid', dest='fdroid',
|
||||
action='store_true', default=False,
|
||||
help='Enable fdroid integration')
|
||||
ap.add_argument('-d', '--debug', dest='debug',
|
||||
action='store_true', default=False,
|
||||
help='Enable debug output')
|
||||
args = ap.parse_args()
|
||||
service = Play(debug=args.debug, fdroid=args.fdroid)
|
||||
app = createServer(service)
|
||||
|
||||
# server setup
|
||||
certfile = os.environ.get('HTTPS_CERTFILE')
|
||||
keyfile = os.environ.get('HTTPS_KEYFILE')
|
||||
server = (httpserver.HTTPServer(app)
|
||||
if certfile is None or keyfile is None else
|
||||
httpserver.HTTPServer(app,
|
||||
ssl_options={'certfile': certfile,
|
||||
'keyfile': keyfile}))
|
||||
server.listen(5000, address='0.0.0.0')
|
||||
|
||||
# credentials setup
|
||||
auth_file_parser = configparser.ConfigParser()
|
||||
auth_file_parser.read('credentials.conf')
|
||||
if 'google' in auth_file_parser:
|
||||
google_section = auth_file_parser['google']
|
||||
if 'email' in google_section and 'password' in google_section:
|
||||
service.set_credentials(google_section['email'], google_section['password'])
|
||||
elif 'gsfId' in google_section and 'token' in google_section:
|
||||
service.set_token_credentials(google_section['gsfId'], google_section['token'])
|
||||
|
||||
if service.has_credentials():
|
||||
service.login()
|
||||
service.update_state()
|
||||
|
||||
# cron task settings
|
||||
cron_string = os.environ.get('CRONTAB_STRING')
|
||||
if cron_string is None:
|
||||
# default is every night at 2AM
|
||||
cron_string = '0 2 * * *'
|
||||
_func = functools.partial(auto_update, *[service])
|
||||
tornado_crontab.CronTabCallback(_func, cron_string).start()
|
||||
io.IOLoop.instance().start()
|
29
playmaker/setup.py
Normal file
29
playmaker/setup.py
Normal file
@ -0,0 +1,29 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(name='playmaker',
|
||||
version='0.6.4',
|
||||
description='Apk manager with web interface based on googleplay-api',
|
||||
url='https://github.com/NoMore201/playmaker',
|
||||
author='NoMore201',
|
||||
author_email='domenico.iezzi.201@gmail.com',
|
||||
license='MIT',
|
||||
packages=['playmaker'],
|
||||
package_data={
|
||||
'playmaker': [
|
||||
'index.html',
|
||||
'static/*',
|
||||
'static/css/*',
|
||||
'static/fonts/*',
|
||||
'static/js/*',
|
||||
'views/*'
|
||||
],
|
||||
},
|
||||
install_requires=[
|
||||
'pyaxmlparser',
|
||||
'pycryptodome',
|
||||
'tornado<5',
|
||||
'gpapi>=0.4.4',
|
||||
'tornado-crontab'
|
||||
],
|
||||
scripts=['pm-server']
|
||||
)
|
Reference in New Issue
Block a user