@ -0,0 +1,30 @@ |
|||||||
|
# Stashr |
||||||
|
Extensible Comic Book file manager and Reader with RESTful API access and control. |
||||||
|
--- |
||||||
|
|
||||||
|
## USER Roles |
||||||
|
- admin |
||||||
|
- librarian |
||||||
|
- patron |
||||||
|
- reader |
||||||
|
|
||||||
|
## Plugin TEP Emits |
||||||
|
- base.html |
||||||
|
- base_page_header_script_files |
||||||
|
- base_page_footer_script_files |
||||||
|
- base_page_script |
||||||
|
- base_page_main_menu |
||||||
|
- base_page_modals |
||||||
|
- read_issue_page.html |
||||||
|
- read_comic_page_modal_extension |
||||||
|
- settings_page |
||||||
|
- settings_menu |
||||||
|
-single_volume_page |
||||||
|
- single_volume_page_modal_have |
||||||
|
- single_volume_page_modal_missing |
||||||
|
- single_volume_page_badge_row |
||||||
|
- single_volume_page_button_row |
||||||
|
- single_volume_page_action_dropdown |
||||||
|
- single_volume_page_top_overlay_have |
||||||
|
- single_volume_page_top_overlay_missing |
||||||
|
- single_volume_page_script |
@ -0,0 +1,61 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Start File |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os, sys |
||||||
|
|
||||||
|
""" --- INSERT LOCAL DIRECTORIES --- """ |
||||||
|
|
||||||
|
base_path = os.path.dirname(os.path.abspath(__file__)) |
||||||
|
|
||||||
|
sys.path.append(os.path.join(base_path, 'stashr')) |
||||||
|
sys.path.append(os.path.join(base_path, 'plugins')) |
||||||
|
|
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr.server import server |
||||||
|
|
||||||
|
debug = True |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- START THE SERVER |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
if debug: |
||||||
|
from stashr.stashr import app |
||||||
|
app.run(host='0.0.0.0', port='5002', debug=True) |
||||||
|
else: |
||||||
|
print('STARTING GEVENT ENVIRONMENT') |
||||||
|
server.start_server() |
@ -0,0 +1,510 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Comicvine Data Retrieval |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import requests |
||||||
|
import requests_cache |
||||||
|
import datetime |
||||||
|
|
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log |
||||||
|
from stashr.config import stashrconfig |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- COMICVINE CLIENT |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
class ComicVineApiError(Exception): |
||||||
|
pass |
||||||
|
|
||||||
|
|
||||||
|
class ComicVineUnauthorizedError(Exception): |
||||||
|
pass |
||||||
|
|
||||||
|
|
||||||
|
class ComicVineForbiddenError(Exception): |
||||||
|
pass |
||||||
|
|
||||||
|
|
||||||
|
class ComicVineClient(object): |
||||||
|
|
||||||
|
API_BASE_URL = 'https://www.comicvine.com/api/' |
||||||
|
|
||||||
|
HEADERS = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:7.0) ' |
||||||
|
'Gecko/20130825 Firefox/36.0'} |
||||||
|
|
||||||
|
SEARCH_RESOURCE_TYPES = { |
||||||
|
'character', 'concept', 'origin', 'object', 'location', 'issue', 'story_arc', |
||||||
|
'volume', 'publisher', 'person', 'team', 'video' |
||||||
|
} |
||||||
|
|
||||||
|
TYPES = { |
||||||
|
'ISSUE': 4000, |
||||||
|
'CHARACTER': 4005, |
||||||
|
'PUBLISHER': 4010, |
||||||
|
'CONCEPT': 4015, |
||||||
|
'LOCATION': 4020, |
||||||
|
'PERSON': 4040, |
||||||
|
'STORY_ARC': 4045, |
||||||
|
'VOLUME': 4050, |
||||||
|
'TEAM': 4060, |
||||||
|
} |
||||||
|
|
||||||
|
ISSUE_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'character_credits', 'characters_died_in', 'concept_credits', 'cover_date', |
||||||
|
'date_added', 'date_last_updated', 'deck', 'description', 'disbanded_teams', 'first_appearance_characters', |
||||||
|
'first_appearance_concepts', 'first_appearance_locations', 'first_appearance_objects', 'first_appearance_teams', |
||||||
|
'has_staff_review', 'id', 'image', 'issue_number', 'location_credits', 'name', 'object_credits', |
||||||
|
'person_credits', 'site_detail_url', 'store_date', 'story_arc_credits', 'team_credits', 'teams_disbanded_in', |
||||||
|
'volume' |
||||||
|
} |
||||||
|
|
||||||
|
ISSUES_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'cover_date', 'date_added', 'date_last_updated', 'deck', 'description', |
||||||
|
'has_staff_review', 'id', 'image', 'issue_number', 'name', 'site_detail_url', 'store_date', 'volume' |
||||||
|
} |
||||||
|
|
||||||
|
CHARACTER_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'birth', 'character_enemies', 'character_friends', 'count_of_issue_appearances', |
||||||
|
'creators', 'date_added', 'date_last_updated', 'deck', 'description', 'first_appeared_in_issue', 'gender', 'id', |
||||||
|
'image', 'issue_credits', 'issues_dies_in', 'movies', 'name', 'origin', 'powers', 'publisher', 'real_name', |
||||||
|
'site_detail_url', 'story_arc_credits', 'team_enemies', 'team_friends', 'teams', 'volume_credits' |
||||||
|
} |
||||||
|
|
||||||
|
PUBLISHER_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'characters', 'date_added', 'date_last_updated', 'deck', 'description', 'id', |
||||||
|
'image', 'location_address', 'location_city', 'location_state', 'name', 'site_detail_url', 'story_arcs', |
||||||
|
'teams', 'volumes' |
||||||
|
} |
||||||
|
|
||||||
|
CONCEPT_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'count_of_issue_appearances', 'date_added', 'date_last_updated', 'deck', |
||||||
|
'description', 'first_appeared_in_issue', 'id', 'image', 'issue_credits', 'movies', 'name', 'site_detail_url', |
||||||
|
'start_year', 'volume_credits' |
||||||
|
} |
||||||
|
|
||||||
|
LOCATION_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'count_of_issue_appearances', 'date_added', 'date_last_updated', 'deck', |
||||||
|
'description', 'first_appeared_in_issue', 'id', 'image', 'issue_credits', 'movies', 'name', 'site_detail_url', |
||||||
|
'start_year', 'story_arc_credits', 'volume_credits' |
||||||
|
} |
||||||
|
|
||||||
|
PERSON_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'birth', 'count_of_issue_appearances', 'country', 'created_characters', |
||||||
|
'date_added', 'date_last_updated', 'death', 'deck', 'description', 'email', 'gender', 'hometown', 'id', 'image', |
||||||
|
'issue_credits', 'name', 'site_detail_url', 'story_arc_credits', 'volume_credits', 'website' |
||||||
|
} |
||||||
|
|
||||||
|
STORY_ARC_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'count_of_issue_appearances', 'date_added', 'date_last_updated', 'deck', |
||||||
|
'description', 'first_appeared_in_issue', 'id', 'image', 'issues', 'movies', 'name', 'publisher', |
||||||
|
'site_detail_url' |
||||||
|
} |
||||||
|
|
||||||
|
VOLUME_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'character_credits', 'concept_credits', 'count_of_issues', 'date_added', |
||||||
|
'date_last_updated', 'deck', 'description', 'first_issue', 'id', 'image', 'last_issue', 'location_credits', |
||||||
|
'name', 'object_credits', 'person_credits', 'publisher', 'site_detail_url', 'start_year', 'team_credits' |
||||||
|
} |
||||||
|
|
||||||
|
TEAM_FIELDS = { |
||||||
|
'aliases', 'api_detail_url', 'character_enemies', 'character_friends', 'characters', |
||||||
|
'count_of_issue_appearances', 'count_of_Team_members', 'date_added', 'date_lat_updated', 'deck', 'description', |
||||||
|
'disbanded_in_issues', 'first_appeared_in_issues', 'id', 'image', 'issue_credits', 'issues_disbanded_in', |
||||||
|
'movies', 'name', 'publisher', 'site_detail_url', 'story_arc_credits', 'volume_credits' |
||||||
|
} |
||||||
|
|
||||||
|
def __init__(self, api_key, expire_after=300): |
||||||
|
self.api_key = api_key |
||||||
|
self._install_requests_cache(expire_after) |
||||||
|
|
||||||
|
def _install_requests_cache(self, expire_after): |
||||||
|
|
||||||
|
requests_cache.install_cache( |
||||||
|
__name__, |
||||||
|
backend='memory', |
||||||
|
expire_after=expire_after |
||||||
|
) |
||||||
|
|
||||||
|
def update_api_key(self, api_key): |
||||||
|
self.api_key = api_key |
||||||
|
|
||||||
|
def search(self, query, offset=0, limit=10, resources=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}search/' |
||||||
|
params = self._get_search_params(query, offset, limit, resources) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_issue(self, id, offset=0, limit=1, fields=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}issue/{self.TYPES["ISSUE"]}-{id}/' |
||||||
|
params = self._get_issue_params(offset, limit, fields) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_character(self, id, offset=0, limit=1, fields=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}character/{self.TYPES["CHARACTER"]}-{id}/' |
||||||
|
params = self._get_character_params(offset, limit, fields) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_publisher(self, id, offset=0, limit=1, fields=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}publisher/{self.TYPES["PUBLISHER"]}-{id}/' |
||||||
|
params = self._get_publisher_params(offset, limit, fields) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_concept(self, id, offset=0, limit=1, fields=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}concept/{self.TYPES["CONCEPT"]}-{id}/' |
||||||
|
params = self._get_concept_params(offset, limit, fields) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_location(self, id, offset=0, limit=1, fields=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}location/{self.TYPES["LOCATION"]}-{id}/' |
||||||
|
params = self._get_location_params(offset, limit, fields) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_person(self, id, offset=0, limit=1, fields=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}person/{self.TYPES["PERSON"]}-{id}/' |
||||||
|
params = self._get_person_params(offset, limit, fields) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_story_arc(self, id, offset=0, limit=1, fields=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}story_arc/{self.TYPES["STORY_ARC"]}-{id}/' |
||||||
|
params = self._get_story_arc_params(offset, limit, fields) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_volume(self, id, offset=0, limit=1, fields=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}volume/{self.TYPES["VOLUME"]}-{id}/' |
||||||
|
params = self._get_volume_params(offset, limit, fields) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_team(self, id, offset=0, limit=1, fields=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}team/{self.TYPES["TEAM"]}-{id}/' |
||||||
|
params = self._get_team_params(offset, limit, fields) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def get_new_releases(self, weekday=2, offset=0, limit=100, resources=None, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}issues/' |
||||||
|
params = self._get_new_releases_params(weekday, offset, limit, resources) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
""" --- GET MULTIPLES OF EACH --- """ |
||||||
|
|
||||||
|
# FILTERS: format, field_list, limit, offset, sort, filter |
||||||
|
|
||||||
|
def get_issues(self, sort=None, filters=None, resources=None, offset=0, limit=100, use_cache=True): |
||||||
|
url = f'{self.API_BASE_URL}issues/' |
||||||
|
params = self._get_issues_params(sort, filters, resources, offset, limit, use_cache) |
||||||
|
json = self._get_from_api(url, params, use_cache) |
||||||
|
return Response(json) |
||||||
|
|
||||||
|
def _get_issues_params(self, sort, filters, resources, offset, limit, use_cache): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'sort': sort, |
||||||
|
'filter': self._validate_issues_filters(filters), |
||||||
|
'field_list': self._validate_issues_resources(resources) |
||||||
|
} |
||||||
|
|
||||||
|
def _validate_issues_resources(self, resources): |
||||||
|
if not resources: |
||||||
|
return None |
||||||
|
valid_resources = self.ISSUES_FIELDS & set(resources) |
||||||
|
return ','.join(valid_resources) if valid_resources else None |
||||||
|
|
||||||
|
def _validate_issues_filters(self, filters): |
||||||
|
if not filters: |
||||||
|
return None |
||||||
|
return_filters = {} |
||||||
|
valid_filters = self.ISSUES_FIELDS & set(filters) |
||||||
|
for valid_filter in valid_filters: |
||||||
|
return_filters[valid_filter] = filters[valid_filter] |
||||||
|
return ','.join(f'{k}:{v}' for k, v in return_filters.items()) if len(return_filters) > 0 else None |
||||||
|
|
||||||
|
""" --- DONE --- """ |
||||||
|
|
||||||
|
def _get_search_params(self, query, offset, limit, resources): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'query': query, |
||||||
|
'resources': self._validate_search_resources(resources) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_issue_params(self, offset, limit, fields): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'field_list': self._validate_issue_fields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_character_params(self, offset, limit, fields): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'field_list': self._validate_character_fields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_publisher_params(self, offset, limit, fields): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'field_list': self._validate_publisher_fields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_concept_params(self, offset, limit, fields): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'field_list': self._validate_concept_fields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_location_params(self, offset, limit, fields): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'field_list': self._validate_location_fields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_person_params(self, offset, limit, fields): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'field_list': self._validate_person_fields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_story_arc_params(self, offset, limit, fields): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'field_list': self._validate_story_arc_fields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_volume_params(self, offset, limit, fields): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'field_list': self._validate_volume_fields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_team_params(self, offset, limit, fields): |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'field_list': self._validate_team_fields(fields) |
||||||
|
} |
||||||
|
|
||||||
|
def _get_new_releases_params(self, weekday, offset, limit, resources): |
||||||
|
|
||||||
|
today_date = datetime.date.today() |
||||||
|
|
||||||
|
delta_days = today_date.weekday() - weekday |
||||||
|
|
||||||
|
if delta_days == 0: |
||||||
|
start_date = today_date |
||||||
|
else: |
||||||
|
start_date = today_date - datetime.timedelta(delta_days) |
||||||
|
|
||||||
|
end_date = start_date + datetime.timedelta(7) |
||||||
|
|
||||||
|
return { |
||||||
|
'api_key': self.api_key, |
||||||
|
'format': 'json', |
||||||
|
'limit': limit, |
||||||
|
'offset': offset, |
||||||
|
'filter': f'store_date:{start_date}|{end_date}' |
||||||
|
} |
||||||
|
|
||||||
|
def _validate_search_resources(self, resources): |
||||||
|
if not resources: |
||||||
|
return None |
||||||
|
valid_resources = self.SEARCH_RESOURCE_TYPES & set(resources) |
||||||
|
return ','.join(valid_resources) if valid_resources else None |
||||||
|
|
||||||
|
def _validate_issue_fields(self, fields): |
||||||
|
if not fields: |
||||||
|
return None |
||||||
|
valid_fields = self.ISSUE_FIELDS & set(fields) |
||||||
|
return ','.join(valid_fields) if valid_fields else None |
||||||
|
|
||||||
|
def _validate_character_fields(self, fields): |
||||||
|
if not fields: |
||||||
|
return None |
||||||
|
valid_fields = self.CHARACTER_FIELDS & set(fields) |
||||||
|
return ','.join(valid_fields) if valid_fields else None |
||||||
|
|
||||||
|
def _validate_publisher_fields(self, fields): |
||||||
|
if not fields: |
||||||
|
return None |
||||||
|
valid_fields = self.PUBLISHER_FIELDS & set(fields) |
||||||
|
return ','.join(valid_fields) if valid_fields else None |
||||||
|
|
||||||
|
def _validate_concept_fields(self, fields): |
||||||
|
if not fields: |
||||||
|
return None |
||||||
|
valid_fields = self.CONCEPT_FIELDS & set(fields) |
||||||
|
return ','.join(valid_fields) if valid_fields else None |
||||||
|
|
||||||
|
def _validate_location_fields(self, fields): |
||||||
|
if not fields: |
||||||
|
return None |
||||||
|
valid_fields = self.LOCATION_FIELDS & set(fields) |
||||||
|
return ','.join(valid_fields) if valid_fields else None |
||||||
|
|
||||||
|
def _validate_person_fields(self, fields): |
||||||
|
if not fields: |
||||||
|
return None |
||||||
|
valid_fields = self.PERSON_FIELDS & set(fields) |
||||||
|
return ','.join(valid_fields) if valid_fields else None |
||||||
|
|
||||||
|
def _validate_story_arc_fields(self, fields): |
||||||
|
if not fields: |
||||||
|
return None |
||||||
|
valid_fields = self.STORY_ARC_FIELDS & set(fields) |
||||||
|
return ','.join(valid_fields) if valid_fields else None |
||||||
|
|
||||||
|
def _validate_volume_fields(self, fields): |
||||||
|
if not fields: |
||||||
|
return None |
||||||
|
valid_fields = self.VOLUME_FIELDS & set(fields) |
||||||
|
return ','.join(valid_fields) if valid_fields else None |
||||||
|
|
||||||
|
def _validate_team_fields(self, fields): |
||||||
|
if not fields: |
||||||
|
return None |
||||||
|
valid_fields = self.TEAM_FIELDS & set(fields) |
||||||
|
return ','.join(valid_fields) if valid_fields else None |
||||||
|
|
||||||
|
def _get_from_api(self, url, params, use_cache): |
||||||
|
|
||||||
|
def _httpget(): |
||||||
|
response = requests.get( |
||||||
|
url, headers=self.HEADERS, params=params |
||||||
|
) |
||||||
|
|
||||||
|
if not response.ok: |
||||||
|
self._handle_http_error(response) |
||||||
|
|
||||||
|
return response.json() |
||||||
|
|
||||||
|
if not use_cache: |
||||||
|
with requests_cache.disabled(): |
||||||
|
return _httpget() |
||||||
|
|
||||||
|
return _httpget() |
||||||
|
|
||||||
|
def _handle_http_error(self, response): |
||||||
|
|
||||||
|
exception = { |
||||||
|
401: ComicVineUnauthorizedError, |
||||||
|
403: ComicVineForbiddenError |
||||||
|
}.get(response.status_code, ComicVineApiError) |
||||||
|
message = f'{response.status_code} {response.reason}' |
||||||
|
|
||||||
|
raise exception(message) |
||||||
|
|
||||||
|
|
||||||
|
class Response(object): |
||||||
|
|
||||||
|
def __init__(self, json): |
||||||
|
|
||||||
|
self.status_code = json.get('status_code', 0) |
||||||
|
self.error = json.get('error', '') |
||||||
|
self.number_of_total_results = json.get('number_of_total_results', 0) |
||||||
|
self.number_of_page_results = json.get('number_of_page_results', 0) |
||||||
|
self.limit = json.get('limit', 0) |
||||||
|
self.offset = json.get('offset', 0) |
||||||
|
self.results = json.get('results', 0) |
||||||
|
self.timestamp = datetime.datetime.utcnow() |
||||||
|
|
||||||
|
@property |
||||||
|
def has_error(self): |
||||||
|
return self.status_code != 1 |
||||||
|
|
||||||
|
def __repr__(self): |
||||||
|
return f'<comicvine_client.response.Response(' \ |
||||||
|
f'status_code={self.status_code!r}) ' \ |
||||||
|
f'object at {hex(id(self))}>' |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- COMICVINE CLIENT DEFINITION |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
cv = ComicVineClient(stashrconfig['APP']['comicvine_api_key']) |
@ -0,0 +1,114 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - [description] |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os, io, shutil, uuid, base64 |
||||||
|
|
||||||
|
from configobj import ConfigObj |
||||||
|
from validate import Validator |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
# from stashr import log |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
# logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- CONFIGURATION |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
class StashrConfig(ConfigObj): |
||||||
|
|
||||||
|
configspec = u""" |
||||||
|
[APP] |
||||||
|
server_port = integer(default=5002) |
||||||
|
log_level = option('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', default='INFO') |
||||||
|
open_registration = boolean(default=False) |
||||||
|
page_length = integer(default=30) |
||||||
|
first_run = boolean(default=True) |
||||||
|
comicvine_api_key = string(default='') |
||||||
|
[DIRECTORY] |
||||||
|
temp = string(default='temp') |
||||||
|
comics = string(default='comics') |
||||||
|
log = string(default='log') |
||||||
|
backup = string(default='backup') |
||||||
|
plugins = string(default='plugins') |
||||||
|
images = string(default='images') |
||||||
|
[API] |
||||||
|
comicvine_api_key = string(default='') |
||||||
|
[SECURITY] |
||||||
|
cookie_secret = string(default='') |
||||||
|
[MAIL] |
||||||
|
mail_username = string(default="") |
||||||
|
mail_password = string(default="") |
||||||
|
mail_default_sender = string(default="") |
||||||
|
mail_server = string(default="") |
||||||
|
mail_port = integer(default=0) |
||||||
|
mail_use_ssl = boolean(default=False) |
||||||
|
[NAMING] |
||||||
|
file = string(default="{volume_name} - {issue_number:0>3}") |
||||||
|
folder = string(default="{volume_name} ({volume_year}) [{volume_id}]") |
||||||
|
""" |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
super(StashrConfig, self).__init__() |
||||||
|
|
||||||
|
if not os.path.exists('configspec.ini'): |
||||||
|
with open('configspec.ini', 'w') as fd: |
||||||
|
shutil.copyfileobj(io.StringIO(StashrConfig.configspec), fd) |
||||||
|
|
||||||
|
self.filename = os.path.join('config.ini') |
||||||
|
self.configspec = os.path.join('configspec.ini') |
||||||
|
self.encoding = "UTF8" |
||||||
|
|
||||||
|
tmp = ConfigObj(self.filename, configspec=self.configspec, encoding=self.encoding) |
||||||
|
validator = Validator() |
||||||
|
tmp.validate(validator, copy=True) |
||||||
|
|
||||||
|
if tmp['SECURITY']['cookie_secret'] == '': |
||||||
|
tmp['SECURITY']['cookie_secret'] = base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes); |
||||||
|
|
||||||
|
self.merge(tmp) |
||||||
|
|
||||||
|
if not os.path.exists(self.filename): |
||||||
|
self.write() |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- CONFIGURATION DEFINITION |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
stashrconfig = StashrConfig() |
@ -0,0 +1,678 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Database Definition |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os |
||||||
|
from datetime import datetime |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log, paths, folders |
||||||
|
# from folders import StashrPaths |
||||||
|
|
||||||
|
""" --- FLASK EXTENSION IMPORTS --- """ |
||||||
|
from flask_login import UserMixin |
||||||
|
from flask_bcrypt import generate_password_hash |
||||||
|
|
||||||
|
""" --- SQLALCHEMY IMPORTS --- """ |
||||||
|
from sqlalchemy import * |
||||||
|
from sqlalchemy.ext.declarative import declarative_base |
||||||
|
from sqlalchemy.orm import * |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
""" --- CREATE SQLITE ENGINE --- """ |
||||||
|
engine = create_engine('sqlite:///{0}'.format(paths.db_path), |
||||||
|
connect_args={'check_same_thread': False, 'timeout': 15}, |
||||||
|
echo=False) |
||||||
|
|
||||||
|
Base = declarative_base() |
||||||
|
|
||||||
|
from marshmallow_sqlalchemy import ModelSchema, SQLAlchemyAutoSchema, SQLAlchemySchema, auto_field |
||||||
|
from marshmallow import fields |
||||||
|
from marshmallow_sqlalchemy.fields import Nested |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- DATABASE |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- RATINGS --- """ |
||||||
|
|
||||||
|
RATING_A = 0 |
||||||
|
RATING_T = 1 |
||||||
|
RATING_T_PLUS = 2 |
||||||
|
RATING_PA = 3 |
||||||
|
RATING_EX = 4 |
||||||
|
RATING_UR = 5 |
||||||
|
|
||||||
|
ratings_dict = [ |
||||||
|
"E", |
||||||
|
"T", |
||||||
|
"T+", |
||||||
|
"PA", |
||||||
|
"Ex", |
||||||
|
"UR", |
||||||
|
] |
||||||
|
|
||||||
|
ratings_dict_words = { |
||||||
|
0: 'Everyone', |
||||||
|
1: 'Teen', |
||||||
|
2: 'Teen+', |
||||||
|
3: 'Parental Advisory', |
||||||
|
4: 'Explicit', |
||||||
|
5: 'Unrated', |
||||||
|
} |
||||||
|
|
||||||
|
reverse_ratings_dict = { |
||||||
|
'Everyone': 0, |
||||||
|
'Teen': 1, |
||||||
|
'Teen+': 2, |
||||||
|
'Parental Advisory': 3, |
||||||
|
'Explicit': 4, |
||||||
|
'Unrated': 5, |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
""" --- DATABASE TABLES --- """ |
||||||
|
|
||||||
|
|
||||||
|
class Users(Base, UserMixin): |
||||||
|
__tablename__ = 'users' |
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
# AUTHENTICATION INFORMATION |
||||||
|
username = Column(String(50), nullable=False, unique=true, server_default='') |
||||||
|
password = Column(String(50), nullable=False, server_default='') |
||||||
|
|
||||||
|
# USER PERMISSIONS |
||||||
|
role = Column(String(100), nullable=False, server_default='reader') |
||||||
|
rating_allowed = Column(Integer, ForeignKey('age_ratings.rating_value'), nullable=False, server_default='0') |
||||||
|
|
||||||
|
# USER EMAIL INFORMATION |
||||||
|
email = Column(String(255), nullable=False, unique=True, server_default='') |
||||||
|
confirmed_at = Column(DateTime()) # REMOVE? |
||||||
|
|
||||||
|
# USER INFORMATION |
||||||
|
is_active = Column(Boolean(), nullable=False, server_default='0') # REMOVE? |
||||||
|
|
||||||
|
# USER API KEY |
||||||
|
api_key = Column(String, server_default='') |
||||||
|
|
||||||
|
# USER LISTS |
||||||
|
user_reading_list = relationship('ReadingLists', backref='user', lazy='dynamic') |
||||||
|
|
||||||
|
# JOINS |
||||||
|
age_rating_title = relationship('AgeRatings', |
||||||
|
primaryjoin='Users.rating_allowed == AgeRatings.rating_value', |
||||||
|
backref='users', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
class Volumes(Base): |
||||||
|
__tablename__ = 'volumes' |
||||||
|
|
||||||
|
volume_id = Column(Integer, ForeignKey('directory_link.directory_volume_id'), primary_key=True) |
||||||
|
|
||||||
|
# VOLUME INFORMATION |
||||||
|
volume_name = Column(String) |
||||||
|
volume_description = Column(String) |
||||||
|
volume_year = Column(String(4)) |
||||||
|
volume_publisher_id = Column(Integer, ForeignKey('publishers.publisher_id')) |
||||||
|
volume_url = Column(String) |
||||||
|
|
||||||
|
# VOLUME IMAGE URL |
||||||
|
volume_image = Column(String) |
||||||
|
|
||||||
|
# VOLUME DETAILS |
||||||
|
volume_latest = Column(String) |
||||||
|
volume_have = Column(String) |
||||||
|
volume_total = Column(String) |
||||||
|
volume_status = Column(Boolean, server_default='0') |
||||||
|
volume_last_update = Column(Date) |
||||||
|
|
||||||
|
# VOLUME STASHR INFO |
||||||
|
volume_path = Column(String) |
||||||
|
volume_age_rating = Column(Integer, server_default=str(RATING_UR)) |
||||||
|
volume_slug = Column(String, unique=True) |
||||||
|
volume_sort_title = Column(String) |
||||||
|
|
||||||
|
# JOINS |
||||||
|
publisher = relationship('Publishers', |
||||||
|
primaryjoin='Volumes.volume_publisher_id == Publishers.publisher_id', |
||||||
|
backref='volumes', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
age_rating = relationship('AgeRatings', |
||||||
|
primaryjoin='Volumes.volume_age_rating == AgeRatings.rating_value', |
||||||
|
backref='volumes', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
directory = relationship('Directories', |
||||||
|
primaryjoin='Volumes.volume_id == Directories.directory_volume_id', |
||||||
|
backref='volumes', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
|
||||||
|
class Issues(Base): |
||||||
|
__tablename__ = 'issues' |
||||||
|
|
||||||
|
issue_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
# FOREIGN KEY RELATION |
||||||
|
issue_volume_id = Column(Integer, ForeignKey('volumes.volume_id')) |
||||||
|
|
||||||
|
# ISSUE INFORMATION |
||||||
|
issue_name = Column(String) |
||||||
|
issue_number = Column(String) |
||||||
|
issue_release_date = Column(String) |
||||||
|
issue_description = Column(String) |
||||||
|
|
||||||
|
# ISSUE IMAGE URL |
||||||
|
issue_cover_url = Column(String) |
||||||
|
|
||||||
|
# ISSUE STASHR DETAILS |
||||||
|
issue_file_status = Column(Boolean, server_default='0') |
||||||
|
issue_file_date = Column(Date) |
||||||
|
issue_file_path = Column(String) |
||||||
|
|
||||||
|
# ADDITIONAL ISSUE INFO |
||||||
|
issue_character_credits = Column(String) |
||||||
|
issue_person_credits = Column(String) |
||||||
|
issue_story_arc_credits = Column(String) |
||||||
|
issue_team_credits = Column(String) |
||||||
|
|
||||||
|
issue_json = Column(String) |
||||||
|
|
||||||
|
# JOINS |
||||||
|
volume = relationship('Volumes', |
||||||
|
primaryjoin='Issues.issue_volume_id == Volumes.volume_id', |
||||||
|
backref='issues', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
read_status = relationship('ReadIssues', |
||||||
|
primaryjoin='Issues.issue_id == ReadIssues.read_issue_id', |
||||||
|
backref='issues') |
||||||
|
|
||||||
|
owned_status = relationship('OwnedIssues', |
||||||
|
primaryjoin='Issues.issue_id == OwnedIssues.owned_issue_id', |
||||||
|
backref='issues') |
||||||
|
|
||||||
|
|
||||||
|
collections = relationship('CollectionLinks', |
||||||
|
primaryjoin='Issues.issue_id == CollectionLinks.collection_link_issue_id', |
||||||
|
backref='issues') |
||||||
|
|
||||||
|
readinglist = relationship('ReadingLists', |
||||||
|
primaryjoin='Issues.issue_id == ReadingLists.reading_list_issue_id', |
||||||
|
backref='issues') |
||||||
|
|
||||||
|
|
||||||
|
class NewReleases(Base): |
||||||
|
__tablename__ = 'new_releases' |
||||||
|
|
||||||
|
new_release_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
new_release_issue_id = Column(Integer, ForeignKey('issues.issue_id')) |
||||||
|
new_release_volume_id = Column(Integer, ForeignKey('volumes.volume_id')) |
||||||
|
|
||||||
|
new_release_comic_name = Column(String) |
||||||
|
new_release_issue_number = Column(String) |
||||||
|
new_release_publisher_id = Column(Integer) |
||||||
|
new_release_publish_date = Column(String) |
||||||
|
|
||||||
|
new_release_item_url = Column(String) |
||||||
|
new_release_image_url = Column(String) |
||||||
|
|
||||||
|
# JOINS |
||||||
|
status = relationship('Volumes', |
||||||
|
primaryjoin='NewReleases.new_release_volume_id == Volumes.volume_id', |
||||||
|
backref='new_releases', |
||||||
|
lazy='select') |
||||||
|
|
||||||
|
publisher = relationship('Publishers', |
||||||
|
primaryjoin='NewReleases.new_release_publisher_id == Publishers.publisher_id', |
||||||
|
backref='new_releases', |
||||||
|
viewonly=True, |
||||||
|
sync_backref=False, |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
|
||||||
|
class ReadIssues(Base): |
||||||
|
__tablename__ = 'issues_read_link' |
||||||
|
|
||||||
|
read_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
read_user_id = Column(Integer, ForeignKey('users.id')) |
||||||
|
read_issue_id = Column(Integer, ForeignKey('issues.issue_id')) |
||||||
|
read_volume_id = Column(Integer, ForeignKey('volumes.volume_id')) |
||||||
|
|
||||||
|
read_status = Column(Boolean, unique=False) |
||||||
|
|
||||||
|
|
||||||
|
class OwnedIssues(Base): |
||||||
|
__tablename__ = 'issues_owned_link' |
||||||
|
|
||||||
|
def as_dict(self): |
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns} |
||||||
|
|
||||||
|
owned_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
owned_user_id = Column(Integer, ForeignKey('users.id')) |
||||||
|
owned_issue_id = Column(Integer, ForeignKey('issues.issue_id')) |
||||||
|
owned_volume_id = Column(Integer, ForeignKey('volumes.volume_id')) |
||||||
|
|
||||||
|
owned_status = Column(Boolean, unique=False) |
||||||
|
|
||||||
|
|
||||||
|
class Collections(Base): |
||||||
|
__tablename__ = 'collections' |
||||||
|
|
||||||
|
collection_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
collection_user_id = Column(Integer, ForeignKey('users.id')) |
||||||
|
|
||||||
|
collection_name = Column(String) |
||||||
|
collection_slug = Column(String) |
||||||
|
collection_sort_title = Column(String) |
||||||
|
collection_public = Column(Boolean, server_default='0') |
||||||
|
|
||||||
|
collection_age_rating = Column(Integer, server_default=str(RATING_UR)) |
||||||
|
|
||||||
|
collection_description = Column(String) |
||||||
|
collection_cover_image = Column(Integer) |
||||||
|
|
||||||
|
user = relationship('Users', |
||||||
|
primaryjoin='Collections.collection_user_id == Users.id', |
||||||
|
backref='collections', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
|
||||||
|
class CollectionLinks(Base): |
||||||
|
__tablename__ = 'collection_issues' |
||||||
|
|
||||||
|
collection_link_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
collection_link_collection_id = Column(Integer, ForeignKey('collections.collection_id')) |
||||||
|
collection_link_issue_id = Column(Integer, ForeignKey('issues.issue_id')) |
||||||
|
collection_link_issue_position = Column(Integer) |
||||||
|
|
||||||
|
issue = relationship('Issues', |
||||||
|
primaryjoin='CollectionLinks.collection_link_issue_id == Issues.issue_id', |
||||||
|
backref='collection_issues', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
collection = relationship('Collections', |
||||||
|
primaryjoin='CollectionLinks.collection_link_collection_id == Collections.collection_id', |
||||||
|
backref='collection_issues', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
class ReadingLists(Base): |
||||||
|
__tablename__ = 'reading_lists' |
||||||
|
|
||||||
|
reading_list_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
reading_list_user_id = Column(Integer, ForeignKey('users.id')) |
||||||
|
reading_list_volume_id = Column(Integer) |
||||||
|
reading_list_issue_id = Column(Integer, ForeignKey('issues.issue_id')) |
||||||
|
reading_list_position = Column(Integer, unique=False) |
||||||
|
|
||||||
|
issue = relationship('Issues', |
||||||
|
primaryjoin='ReadingLists.reading_list_issue_id == Issues.issue_id', |
||||||
|
backref='reading_lists', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
|
||||||
|
class Characters(Base): |
||||||
|
__tablename__ = 'characters' |
||||||
|
|
||||||
|
character_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
character_name = Column(String) |
||||||
|
character_description = Column(String) |
||||||
|
character_image = Column(String) |
||||||
|
character_issue_credits = Column(String) |
||||||
|
character_volume_credits = Column(String) |
||||||
|
|
||||||
|
|
||||||
|
class People(Base): |
||||||
|
__tablename__ = 'people' |
||||||
|
|
||||||
|
person_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
person_name = Column(String) |
||||||
|
person_image = Column(String) |
||||||
|
person_website = Column(String) |
||||||
|
person_description = Column(String) |
||||||
|
|
||||||
|
|
||||||
|
class Publishers(Base): |
||||||
|
__tablename__ = 'publishers' |
||||||
|
|
||||||
|
publisher_id = Column(Integer, ForeignKey('new_releases.new_release_publisher_id'), primary_key=True) |
||||||
|
|
||||||
|
publisher_image = Column(String) |
||||||
|
publisher_name = Column(String) |
||||||
|
publisher_description = Column(String) |
||||||
|
|
||||||
|
|
||||||
|
class StoryArcs(Base): |
||||||
|
__tablename__ = 'story_arcs' |
||||||
|
|
||||||
|
story_arc_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
story_arc_description = Column(String) |
||||||
|
story_arc_image = Column(String) |
||||||
|
story_arc_count_of_issues = Column(Integer) |
||||||
|
story_arc_issues = Column(String) |
||||||
|
|
||||||
|
|
||||||
|
class AgeRatings(Base): |
||||||
|
__tablename__ = 'age_ratings' |
||||||
|
|
||||||
|
rating_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
rating_value = Column(Integer, ForeignKey('volumes.volume_age_rating')) |
||||||
|
rating_short = Column(String) |
||||||
|
rating_long = Column(String) |
||||||
|
|
||||||
|
|
||||||
|
class Libraries(Base): |
||||||
|
__tablename__ = 'libraries' |
||||||
|
|
||||||
|
library_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
library_default = Column(Boolean, server_default='0') |
||||||
|
library_path = Column(String) |
||||||
|
|
||||||
|
class Directories(Base): |
||||||
|
__tablename__ = 'directory_link' |
||||||
|
|
||||||
|
directory_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
directory_volume_id = Column(Integer, ForeignKey('volumes.volume_id'), unique=True) |
||||||
|
directory_path= Column(String) |
||||||
|
directory_in_library = Column(Boolean, server_default='0') |
||||||
|
|
||||||
|
|
||||||
|
class ScrapeItems(Base): |
||||||
|
__tablename__ = 'scrape_items' |
||||||
|
|
||||||
|
scrape_id = Column(Integer, primary_key=True) |
||||||
|
|
||||||
|
scrape_directory_id = Column(Integer, ForeignKey('directory_link.directory_id')) |
||||||
|
scrape_directory = Column(String, unique=True) |
||||||
|
|
||||||
|
scrape_match = Column(Boolean, server_default='0') |
||||||
|
scrape_add = Column(Boolean, server_default='0') |
||||||
|
scrape_candidate = Column(Integer) |
||||||
|
scrape_json = Column(String) |
||||||
|
|
||||||
|
directory = relationship('Directories', |
||||||
|
primaryjoin='ScrapeItems.scrape_directory_id == Directories.directory_id', |
||||||
|
backref='scrape_items', |
||||||
|
lazy='joined') |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- DATABASE SCHEMAS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
class PublishersSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
include_fk = True |
||||||
|
model = Publishers |
||||||
|
|
||||||
|
|
||||||
|
class AgeRatingsSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
include_fk = True |
||||||
|
model = AgeRatings |
||||||
|
|
||||||
|
|
||||||
|
class UsersSchema(SQLAlchemySchema): |
||||||
|
class Meta: |
||||||
|
include_fk = True |
||||||
|
model = Users |
||||||
|
|
||||||
|
id = auto_field() |
||||||
|
username = auto_field() |
||||||
|
role = auto_field() |
||||||
|
rating_allowed = auto_field() |
||||||
|
email = auto_field() |
||||||
|
|
||||||
|
age_rating_title = Nested(AgeRatingsSchema) |
||||||
|
|
||||||
|
|
||||||
|
class DirectoriesSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = Directories |
||||||
|
|
||||||
|
|
||||||
|
class VolumesSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
include_fk = True |
||||||
|
model = Volumes |
||||||
|
|
||||||
|
publisher = Nested(PublishersSchema) |
||||||
|
age_rating = Nested(AgeRatingsSchema, many=True) |
||||||
|
directory = Nested(DirectoriesSchema) |
||||||
|
|
||||||
|
|
||||||
|
class NewReleasesSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
include_fk = True |
||||||
|
model = NewReleases |
||||||
|
|
||||||
|
status = Nested(VolumesSchema) |
||||||
|
publisher = Nested(PublishersSchema) |
||||||
|
|
||||||
|
|
||||||
|
class ReadIssuesSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = ReadIssues |
||||||
|
include_fk = True |
||||||
|
|
||||||
|
|
||||||
|
class OwnedIssuesSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = OwnedIssues |
||||||
|
|
||||||
|
|
||||||
|
class CollectionsSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = Collections |
||||||
|
|
||||||
|
user = Nested(UsersSchema) |
||||||
|
|
||||||
|
|
||||||
|
class IssuesSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = Issues |
||||||
|
|
||||||
|
volume = Nested(VolumesSchema) |
||||||
|
read_status = Nested(ReadIssuesSchema, many=True) |
||||||
|
owned_status = Nested(OwnedIssuesSchema, many=True) |
||||||
|
|
||||||
|
|
||||||
|
class CollectionLinksSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = CollectionLinks |
||||||
|
|
||||||
|
issue = Nested(IssuesSchema) |
||||||
|
collection = Nested(CollectionsSchema) |
||||||
|
|
||||||
|
|
||||||
|
class ReadingListsSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = ReadingLists |
||||||
|
|
||||||
|
issue = Nested(IssuesSchema) |
||||||
|
|
||||||
|
|
||||||
|
class CharactersSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = Characters |
||||||
|
|
||||||
|
|
||||||
|
class PeopleSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = People |
||||||
|
|
||||||
|
|
||||||
|
class StoryArcsSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = StoryArcs |
||||||
|
|
||||||
|
|
||||||
|
class ScrapeItemsSchema(SQLAlchemyAutoSchema): |
||||||
|
class Meta: |
||||||
|
model = ScrapeItems |
||||||
|
|
||||||
|
directory = Nested(DirectoriesSchema) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- DATABASE DEFINITION |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
Session = sessionmaker(bind=engine) |
||||||
|
session = Session() |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- DATABASE FUNCTIONS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
# Create Default Admin Account |
||||||
|
def create_default_admin(): |
||||||
|
|
||||||
|
user = Users() |
||||||
|
|
||||||
|
user.username = 'admin' |
||||||
|
user.password = generate_password_hash('admin123').decode('utf-8') |
||||||
|
user.role = 'admin' |
||||||
|
user.email = 'no@email.com' |
||||||
|
user.confirmed_at = datetime.now() |
||||||
|
user.is_active = True |
||||||
|
user.rating_allowed = RATING_UR |
||||||
|
|
||||||
|
session.add(user) |
||||||
|
session.commit() |
||||||
|
|
||||||
|
|
||||||
|
def create_default_age_ratings(): |
||||||
|
|
||||||
|
json = [] |
||||||
|
|
||||||
|
rating_e = { |
||||||
|
"rating_value": 0, |
||||||
|
"rating_short": 'E', |
||||||
|
"rating_long": 'Everyone' |
||||||
|
} |
||||||
|
rating_t = { |
||||||
|
"rating_value": 1, |
||||||
|
"rating_short": 'T', |
||||||
|
"rating_long": 'Teen' |
||||||
|
} |
||||||
|
rating_t_plus = { |
||||||
|
"rating_value":2, |
||||||
|
"rating_short": 'T+', |
||||||
|
"rating_long": 'Teen+' |
||||||
|
} |
||||||
|
rating_pa = { |
||||||
|
"rating_value": 3, |
||||||
|
"rating_short": 'PA', |
||||||
|
"rating_long": 'Parental Advisory' |
||||||
|
} |
||||||
|
rating_ex = { |
||||||
|
"rating_value": 4, |
||||||
|
"rating_short": 'Ex', |
||||||
|
"rating_long": 'Explicit' |
||||||
|
} |
||||||
|
rating_ur = { |
||||||
|
"rating_value": 5, |
||||||
|
"rating_short": 'UR', |
||||||
|
"rating_long": 'Unrated' |
||||||
|
} |
||||||
|
|
||||||
|
json.append(rating_e) |
||||||
|
json.append(rating_t) |
||||||
|
json.append(rating_t_plus) |
||||||
|
json.append(rating_pa) |
||||||
|
json.append(rating_ex) |
||||||
|
json.append(rating_ur) |
||||||
|
|
||||||
|
print(json) |
||||||
|
|
||||||
|
for rating in json: |
||||||
|
new_rating = AgeRatings() |
||||||
|
new_rating.rating_value = rating['rating_value'] |
||||||
|
new_rating.rating_short = rating['rating_short'] |
||||||
|
new_rating.rating_long = rating['rating_long'] |
||||||
|
|
||||||
|
session.add(new_rating) |
||||||
|
|
||||||
|
session.commit() |
||||||
|
|
||||||
|
|
||||||
|
# Migrate Database |
||||||
|
def migrate_database(): |
||||||
|
# CHECK FOR NEW DATABASE TABLES |
||||||
|
if not engine.dialect.has_table(engine.connect(), "directory_link"): |
||||||
|
Directories.__table__.create(bind=engine) |
||||||
|
# CHECK FOR NEW TABLE COLUMNS |
||||||
|
|
||||||
|
|
||||||
|
# Create Database |
||||||
|
if not os.path.exists(folders.StashrPaths().db_path()): |
||||||
|
print(f'PATH: {folders.StashrPaths().db_path()}') |
||||||
|
try: |
||||||
|
logger.debug('Creating Database') |
||||||
|
Base.metadata.create_all(engine) |
||||||
|
create_default_admin() |
||||||
|
create_default_age_ratings() |
||||||
|
except Exception as e: |
||||||
|
logger.error('Problem Creating Database') |
||||||
|
logger.error(e) |
||||||
|
raise e |
||||||
|
else: |
||||||
|
logger.debug('Database Exists') |
||||||
|
try: |
||||||
|
Base.metadata.create_all(engine) |
||||||
|
migrate_database() |
||||||
|
except Exception as e: |
||||||
|
logger.error('Problem Accessing Database') |
||||||
|
logger.error(e) |
@ -0,0 +1,173 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - [description] |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os |
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
from stashr.config import stashrconfig |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- PATHS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
class StashrPaths: |
||||||
|
|
||||||
|
|
||||||
|
def base_path(self): |
||||||
|
return os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + '..' + os.sep) |
||||||
|
|
||||||
|
|
||||||
|
def db_path(self): |
||||||
|
return os.path.join( |
||||||
|
self.base_path(), |
||||||
|
'database.db' |
||||||
|
) |
||||||
|
|
||||||
|
# base_path = os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + '..' + os.sep) |
||||||
|
# db_path = os.path.join(base_path, 'database.db') |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- FOLDERS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
class StashrFolders: |
||||||
|
|
||||||
|
|
||||||
|
def temp_folder(self): |
||||||
|
|
||||||
|
folder = os.path.join( |
||||||
|
StashrPaths().base_path(), |
||||||
|
stashrconfig['DIRECTORY']['temp'] |
||||||
|
) |
||||||
|
|
||||||
|
if not os.path.isdir(folder): |
||||||
|
logger.debug('Creating Temp Folder') |
||||||
|
os.mkdir(folder) |
||||||
|
|
||||||
|
return folder |
||||||
|
|
||||||
|
|
||||||
|
def comic_folder(self): |
||||||
|
|
||||||
|
folder = os.path.join( |
||||||
|
StashrPaths().base_path(), |
||||||
|
stashrconfig['DIRECTORY']['comics'] |
||||||
|
) |
||||||
|
|
||||||
|
logger.debug(folder) |
||||||
|
|
||||||
|
if not os.path.isdir(folder): |
||||||
|
logger.info('Creating Volumes Folder') |
||||||
|
os.mkdir(folder) |
||||||
|
|
||||||
|
return folder |
||||||
|
|
||||||
|
|
||||||
|
def log_folder(self): |
||||||
|
|
||||||
|
folder = os.path.join( |
||||||
|
StashrPaths().base_path(), |
||||||
|
stashrconfig['DIRECTORY']['log'] |
||||||
|
) |
||||||
|
|
||||||
|
if not os.path.isdir(folder): |
||||||
|
logger.info('Creating Log Folder') |
||||||
|
os.mkdir(folder) |
||||||
|
|
||||||
|
return folder |
||||||
|
|
||||||
|
|
||||||
|
def backup_folder(self): |
||||||
|
|
||||||
|
folder = os.path.join( |
||||||
|
StashrPaths().base_path(), |
||||||
|
stashrconfig['DIRECTORY']['backup'] |
||||||
|
) |
||||||
|
|
||||||
|
if not os.path.isdir(folder): |
||||||
|
logger.info('Creating Backup Folder') |
||||||
|
os.mkdir(folder) |
||||||
|
|
||||||
|
return folder |
||||||
|
|
||||||
|
|
||||||
|
def plugins_folder(self): |
||||||
|
|
||||||
|
folder = os.path.join( |
||||||
|
StashrPaths().base_path(), |
||||||
|
stashrconfig['DIRECTORY']['plugins'] |
||||||
|
) |
||||||
|
|
||||||
|
if not os.path.isdir(folder): |
||||||
|
logger.info('Creating Plugins Folder') |
||||||
|
os.mkdir(folder) |
||||||
|
|
||||||
|
return folder |
||||||
|
|
||||||
|
|
||||||
|
def images_folder(self): |
||||||
|
|
||||||
|
folder = os.path.join( |
||||||
|
StashrPaths().base_path(), |
||||||
|
stashrconfig['DIRECTORY']['images'] |
||||||
|
) |
||||||
|
|
||||||
|
if not os.path.isdir(folder): |
||||||
|
logger.info('Creating Images Folder') |
||||||
|
os.mkdir(folder) |
||||||
|
|
||||||
|
return folder |
||||||
|
|
||||||
|
|
||||||
|
def covers_folder(self): |
||||||
|
|
||||||
|
folder = os.path.join( |
||||||
|
StashrPaths().base_path(), |
||||||
|
stashrconfig['DIRECTORY']['covers'] |
||||||
|
) |
||||||
|
|
||||||
|
if not os.path.isdir(folder): |
||||||
|
logger.info('Creating Covers Folder') |
||||||
|
os.mkdir(folder) |
||||||
|
|
||||||
|
return folder |
@ -0,0 +1,603 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Forms |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log, database |
||||||
|
|
||||||
|
""" --- FLASK EXTENSION IMPORTS --- """ |
||||||
|
from flask_wtf import FlaskForm |
||||||
|
from flask_wtf.file import FileField, FileRequired, FileAllowed |
||||||
|
|
||||||
|
from wtforms import StringField, BooleanField, SelectField, IntegerField, HiddenField, TextAreaField, SubmitField |
||||||
|
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError |
||||||
|
|
||||||
|
from flask_bcrypt import check_password_hash, generate_password_hash |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- FORM VALIDATIONS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
VALID_ROLES = [ 'admin', |
||||||
|
'librarian', |
||||||
|
'patron', |
||||||
|
'reader' |
||||||
|
] |
||||||
|
|
||||||
|
VALID_RATINGS = [ 'Everyone', |
||||||
|
'Teen', |
||||||
|
'Teen+', |
||||||
|
'Parental Advisory', |
||||||
|
'Explicit', |
||||||
|
'Unrated' |
||||||
|
] |
||||||
|
|
||||||
|
VALID_LOGGING_LEVELS = [ 'CRITICAL', |
||||||
|
'ERROR', |
||||||
|
'WARNING', |
||||||
|
'INFO', |
||||||
|
'DEBUG'] |
||||||
|
|
||||||
|
def check_user_email(form, field): |
||||||
|
if database.session.query(database.Users).filter( (database.Users.username==field.data) | (database.Users.email==field.data) ).first() is None: |
||||||
|
raise ValidationError('Username/Email not found') |
||||||
|
|
||||||
|
|
||||||
|
def dup_username_check(form, field): |
||||||
|
if database.session.query(database.Users).filter_by(username=field.data).first() is not None: |
||||||
|
raise ValidationError('Username already exists') |
||||||
|
|
||||||
|
|
||||||
|
def dup_email_check(form, field): |
||||||
|
if database.session.query(database.Users).filter_by(email=field.data).first() is not None: |
||||||
|
raise ValidationError('Email already has an account') |
||||||
|
|
||||||
|
|
||||||
|
def check_existing_email(form, field): |
||||||
|
if database.session.query(database.Users).filter_by(email=field.data).first() is None: |
||||||
|
raise ValidationError('Email not associated with account') |
||||||
|
|
||||||
|
|
||||||
|
def update_email_check(form, field): |
||||||
|
# print(database.session.query(database.Users).filter_by(email=field.data).first().id) |
||||||
|
if int(database.session.query(database.Users).filter_by(email=field.data).first().id) != int(form.user_id.data): |
||||||
|
raise ValidationError('Email associated with a different account') |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- FORMS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- USER FORMS --- """ |
||||||
|
|
||||||
|
|
||||||
|
# LOGIN FORM |
||||||
|
class login_form(FlaskForm): |
||||||
|
username = StringField( |
||||||
|
'Username/Email', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a Username'), |
||||||
|
check_user_email |
||||||
|
] |
||||||
|
) |
||||||
|
password = StringField( |
||||||
|
'Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a password') |
||||||
|
] |
||||||
|
) |
||||||
|
remember_me = BooleanField( |
||||||
|
'Remember Me', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
login_button = SubmitField( |
||||||
|
'Login' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# REGISTRATION FORM |
||||||
|
class registration_form(FlaskForm): |
||||||
|
username = StringField( |
||||||
|
'Username', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a Username'), |
||||||
|
Length(min=5, message='Username must be at least 5 characters'), |
||||||
|
dup_username_check |
||||||
|
] |
||||||
|
) |
||||||
|
email = StringField( |
||||||
|
'Email', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter your email address'), |
||||||
|
Email(message='Please enter a valid email address'), |
||||||
|
dup_email_check |
||||||
|
] |
||||||
|
) |
||||||
|
reg_password = StringField( |
||||||
|
'Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a password'), |
||||||
|
Length(min=8, message='Password must be at least 8 characters') |
||||||
|
] |
||||||
|
) |
||||||
|
confirm_reg_password = StringField( |
||||||
|
'Confirm Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please confirm your password'), |
||||||
|
Length(min=8), |
||||||
|
EqualTo('reg_password', message='Passwords must match') |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
register_button = SubmitField( |
||||||
|
'Register' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# FORGOT PASSWORD FORM |
||||||
|
class forgot_password_form(FlaskForm): |
||||||
|
email = StringField( |
||||||
|
'Email Address', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter your email address'), |
||||||
|
Email(message='Please enter a valid email address'), |
||||||
|
check_existing_email |
||||||
|
] |
||||||
|
) |
||||||
|
forgot_button = SubmitField( |
||||||
|
'Send Email' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class new_user_form(FlaskForm): |
||||||
|
|
||||||
|
username = StringField( |
||||||
|
'Username', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a Username'), |
||||||
|
Length(min=5, message='Username must be at least 5 characters'), |
||||||
|
dup_username_check |
||||||
|
] |
||||||
|
) |
||||||
|
email = StringField( |
||||||
|
'Email', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter an email address'), |
||||||
|
Email(message='Please enter a valid email address'), |
||||||
|
dup_email_check |
||||||
|
] |
||||||
|
) |
||||||
|
password = StringField( |
||||||
|
'Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a password'), |
||||||
|
Length(min=8, message='Password must be at least 8 characters') |
||||||
|
] |
||||||
|
) |
||||||
|
confirm_password = StringField( |
||||||
|
'Confirm Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please confirm your password'), |
||||||
|
Length(min=8), |
||||||
|
EqualTo('password', message='Passwords must match') |
||||||
|
] |
||||||
|
) |
||||||
|
role = SelectField( |
||||||
|
'Role', |
||||||
|
choices = [ |
||||||
|
(role, role) for role in VALID_ROLES |
||||||
|
] |
||||||
|
) |
||||||
|
age_rating = SelectField( |
||||||
|
'Age Rating', |
||||||
|
choices = [ |
||||||
|
(rating[0], rating[1]) for rating in database.ratings_dict_words.items() |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
new_user_button = SubmitField( |
||||||
|
'Create User' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- SETTINGS FORMS --- """ |
||||||
|
|
||||||
|
|
||||||
|
# UPDATE APP SETTINGS |
||||||
|
class update_app_settings_form(FlaskForm): |
||||||
|
# open_registration - boolean |
||||||
|
open_registration = BooleanField( |
||||||
|
'Open Registration', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
# server_port - int |
||||||
|
server_port = IntegerField( |
||||||
|
'Server Port', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
# api_comicvine - string |
||||||
|
comicvine_api_key = StringField( |
||||||
|
'Comicvine API Key', |
||||||
|
validators=[ |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
# debug level |
||||||
|
log_level = SelectField( |
||||||
|
'Logging Level', |
||||||
|
choices = [ |
||||||
|
(level, level) for level in VALID_LOGGING_LEVELS |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
update_app_button = SubmitField( |
||||||
|
'Update' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# UPDATE DIRECTORY SETTINGS |
||||||
|
class update_directory_settings_form(FlaskForm): |
||||||
|
# temp |
||||||
|
temp_directory = StringField( |
||||||
|
'Temp Directory', |
||||||
|
validators=[ |
||||||
|
] |
||||||
|
) |
||||||
|
# comics |
||||||
|
comics_directory = StringField( |
||||||
|
'Comics Directory', |
||||||
|
validators=[ |
||||||
|
] |
||||||
|
) |
||||||
|
# log |
||||||
|
log_directory = StringField( |
||||||
|
'Log Directory', |
||||||
|
validators=[ |
||||||
|
] |
||||||
|
) |
||||||
|
# backup |
||||||
|
backup_directory = StringField( |
||||||
|
'Backup Directory', |
||||||
|
validators=[ |
||||||
|
] |
||||||
|
) |
||||||
|
# plugins |
||||||
|
plugins_directory = StringField( |
||||||
|
'Plugins Directory', |
||||||
|
validators=[ |
||||||
|
] |
||||||
|
) |
||||||
|
# images |
||||||
|
images_directory = StringField( |
||||||
|
'Images Directory', |
||||||
|
validators=[ |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
update_directory_button = SubmitField( |
||||||
|
'Update' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# UPDATE MAIL SETTINGS |
||||||
|
class update_mail_settings_form(FlaskForm): |
||||||
|
|
||||||
|
mail_use = BooleanField( |
||||||
|
'Use Mail', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
# mail_username - string |
||||||
|
mail_username = StringField( |
||||||
|
'Username', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
# mail_password - string |
||||||
|
mail_password = StringField( |
||||||
|
'Password', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
# mail_default_sender - string |
||||||
|
mail_default_sender = StringField( |
||||||
|
'Default Sender', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
# mail_server - string |
||||||
|
mail_server = StringField( |
||||||
|
'Mail Server', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
# mail_port - integer |
||||||
|
mail_port = IntegerField( |
||||||
|
'Port', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
# mail_use_ssl - boolean |
||||||
|
mail_use_ssl = BooleanField( |
||||||
|
'Use SSL', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
update_mail_button = SubmitField( |
||||||
|
'Update' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# APP FIRST RUN FORM |
||||||
|
class app_first_run_form(FlaskForm): |
||||||
|
|
||||||
|
username = StringField( |
||||||
|
'Admin Username', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Administrative Username Required') |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
email = StringField( |
||||||
|
'E-Mail', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Administrative E-Mail Required'), |
||||||
|
Email(message='Please enter a valid email address'), |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
password = StringField( |
||||||
|
'Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a password'), |
||||||
|
Length(min=8, message='Password must be at least 8 characters') |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
confirm_password = StringField( |
||||||
|
'Confirm Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a password'), |
||||||
|
Length(min=8, message='Password must be at least 8 characters'), |
||||||
|
EqualTo('password', message='Passwords must match') |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
comicvine_api_key = StringField( |
||||||
|
'Comivine API Key', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter your Comicvine API Key') |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
open_registration = BooleanField( |
||||||
|
'Enable Open Registration', |
||||||
|
validators = [] |
||||||
|
) |
||||||
|
|
||||||
|
logging_level = SelectField( |
||||||
|
'Logging Level', |
||||||
|
choices=[ |
||||||
|
(level, level) for level in VALID_LOGGING_LEVELS |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
first_run_button = SubmitField( |
||||||
|
'Save Settings' |
||||||
|
) |
||||||
|
|
||||||
|
""" |
||||||
|
# App Logging |
||||||
|
app_logging = BooleanField( |
||||||
|
'Enable App Logging', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
# App Open Registration |
||||||
|
app_open_registration = BooleanField( |
||||||
|
'Enable Open Registration', |
||||||
|
validators = [ |
||||||
|
] |
||||||
|
) |
||||||
|
# api_comicvine - string |
||||||
|
api_comicvine = StringField( |
||||||
|
'Comicvine API', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter your Comicvine API Key') |
||||||
|
] |
||||||
|
) |
||||||
|
email = StringField( |
||||||
|
'Admin Email', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter your email address'), |
||||||
|
Email(message='Please enter a valid email address'), |
||||||
|
dup_email_check |
||||||
|
] |
||||||
|
) |
||||||
|
app_logging_level = SelectField( |
||||||
|
'Logging Level', |
||||||
|
choices=[ |
||||||
|
(level, level) for level in VALID_LOGGING_LEVELS |
||||||
|
] |
||||||
|
) |
||||||
|
admin_fr_password = StringField( |
||||||
|
'Admin Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a password'), |
||||||
|
Length(min=8, message='Password must be at least 8 characters') |
||||||
|
] |
||||||
|
) |
||||||
|
confirm_admin_fr_password = StringField( |
||||||
|
'Confirm Admin Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please confirm your password'), |
||||||
|
Length(min=8), |
||||||
|
EqualTo('admin_fr_password', message='Passwords must match') |
||||||
|
] |
||||||
|
) |
||||||
|
submit_first_button = SubmitField( |
||||||
|
'Save' |
||||||
|
) |
||||||
|
""" |
||||||
|
|
||||||
|
# Change Password Form |
||||||
|
class change_password_form(FlaskForm): |
||||||
|
|
||||||
|
def __init__(self, user, *args, **kwargs): |
||||||
|
super(change_password_form, self).__init__(*args, **kwargs) |
||||||
|
self.user = user |
||||||
|
|
||||||
|
old_password = StringField( |
||||||
|
'Old Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter your password'), |
||||||
|
Length(min=8, message='Password must be at least 8 characters') |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
new_password = StringField( |
||||||
|
'New Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a new password'), |
||||||
|
Length(min=8, message='Password must be at least 8 characters') |
||||||
|
] |
||||||
|
) |
||||||
|
confirm_password = StringField( |
||||||
|
'Confirm New Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please confirm your new password'), |
||||||
|
Length(min=8, message='Password must be at least 8 characters'), |
||||||
|
EqualTo('new_password', message='Passwords must match') |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
update_password_button = SubmitField( |
||||||
|
'Update Password' |
||||||
|
) |
||||||
|
|
||||||
|
def validate_old_password(self, field): |
||||||
|
|
||||||
|
if not check_password_hash(self.user.password, field.data): |
||||||
|
raise ValidationError('Old Password Not Correct') |
||||||
|
|
||||||
|
|
||||||
|
# Reset Password Form |
||||||
|
class reset_password_form(FlaskForm): |
||||||
|
|
||||||
|
def __init__(self, user, *args, **kwargs): |
||||||
|
super(reset_password_form, self).__init__(*args, **kwargs) |
||||||
|
self.user = user |
||||||
|
|
||||||
|
password = StringField( |
||||||
|
'Reset Password', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter a new password'), |
||||||
|
Length(min=8, message='Password must be at least 8 characters') |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
reset_password_button = SubmitField( |
||||||
|
'Reset Password' |
||||||
|
) |
||||||
|
|
||||||
|
class delete_user_form(FlaskForm): |
||||||
|
|
||||||
|
def __init__(self, user, *args, **kwargs): |
||||||
|
super(delete_user_form, self).__init__(*args, **kwargs) |
||||||
|
self.user = user |
||||||
|
|
||||||
|
delete_user_button = SubmitField( |
||||||
|
'Delete User' |
||||||
|
) |
||||||
|
|
||||||
|
class edit_user_form(FlaskForm): |
||||||
|
|
||||||
|
def __init__(self, user, *args, **kwargs): |
||||||
|
super(edit_user_form, self).__init__(*args, **kwargs) |
||||||
|
self.user = user |
||||||
|
|
||||||
|
email = StringField( |
||||||
|
'User Email', |
||||||
|
validators = [ |
||||||
|
DataRequired(message='Please enter your email address'), |
||||||
|
Email(message='Please enter a valid email address'), |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
role = SelectField( |
||||||
|
'Role', |
||||||
|
choices = [ |
||||||
|
(role, role) for role in VALID_ROLES |
||||||
|
] |
||||||
|
) |
||||||
|
age_rating = SelectField( |
||||||
|
'Age Rating', |
||||||
|
choices = [ |
||||||
|
(rating[0], rating[1]) for rating in database.ratings_dict_words.items() |
||||||
|
] |
||||||
|
) |
||||||
|
|
||||||
|
edit_user_button = SubmitField( |
||||||
|
'Edit User' |
||||||
|
) |
||||||
|
|
||||||
|
def validate_email(self, field): |
||||||
|
if field.data != self.user.email and database.session.query(database.Users).filter_by(email=field.data).first() is not None: |
||||||
|
raise ValidationError('Email already has an account') |
||||||
|
|
||||||
|
""" --- UPLOAD FORMS --- """ |
||||||
|
|
||||||
|
# UPLOAD PLUGIN FORM |
||||||
|
class upload_plugin_form(FlaskForm): |
||||||
|
plugin = FileField( |
||||||
|
'Upload Plugin Zip', |
||||||
|
validators=[FileRequired(), |
||||||
|
FileAllowed(['zip'], 'ZIP File Format Required')] |
||||||
|
) |
||||||
|
upload_plugin_button = SubmitField( |
||||||
|
'Upload Plugin' |
||||||
|
) |
@ -0,0 +1,80 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Custom Log Handling |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os, datetime, logging |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import paths |
||||||
|
|
||||||
|
""" --- CONFIG IMPORTS --- """ |
||||||
|
from stashr.config import stashrconfig |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- CUSTOM LOGGERS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
def stashr_logger(name): |
||||||
|
|
||||||
|
log_location = os.path.join(paths.base_path, 'log') |
||||||
|
log_file = os.path.join(log_location, 'log-{0}.txt'.format(datetime.date.today())) |
||||||
|
|
||||||
|
if not os.path.exists(log_location): |
||||||
|
os.mkdir(log_location) |
||||||
|
|
||||||
|
if not os.path.exists(log_file): |
||||||
|
file = open(log_file, 'w') |
||||||
|
file.close |
||||||
|
|
||||||
|
formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(module)s - %(message)s') |
||||||
|
|
||||||
|
handler = logging.StreamHandler() |
||||||
|
handler.setFormatter(formatter) |
||||||
|
|
||||||
|
logger = logging.getLogger(name) |
||||||
|
logger.setLevel(stashrconfig['APP']['log_level']) |
||||||
|
logger.addHandler(handler) |
||||||
|
|
||||||
|
logging.basicConfig(filename=log_file, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s') |
||||||
|
|
||||||
|
return logger |
||||||
|
|
||||||
|
def disable_huey_logger(): |
||||||
|
huey_logger = logging.getLogger("huey") |
||||||
|
huey_logger.setLevel(logging.CRITICAL) |
||||||
|
if huey_logger.handlers: |
||||||
|
huey_logger.handlers.pop() |
@ -0,0 +1,132 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - File Naming |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
from pathvalidate import sanitize_filename |
||||||
|
|
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log, database |
||||||
|
from stashr.config import stashrconfig |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- FILE AND FOLDER NAMING |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
def volume_folder_name_from_cv_item(volume): |
||||||
|
|
||||||
|
# name_scheme = "{volume_name} ({volume_year}) [{volume_id}]" |
||||||
|
name_scheme = stashrconfig['NAMING']['folder'] |
||||||
|
|
||||||
|
print(volume.results['name']) |
||||||
|
|
||||||
|
return sanitize_filename( |
||||||
|
name_scheme.format( |
||||||
|
volume_id = volume.results['id'], |
||||||
|
volume_name = volume.results['name'], |
||||||
|
volume_year = volume.results['start_year'], |
||||||
|
volume_publisher_id = volume.results['publisher']['id'], |
||||||
|
publisher_name = volume.results['publisher']['name'], |
||||||
|
)) |
||||||
|
|
||||||
|
|
||||||
|
def folder_name(volume_id): |
||||||
|
|
||||||
|
# name_scheme = "{volume_name} ({volume_year}) [{volume_id}]" |
||||||
|
name_scheme = stashrconfig['NAMING']['folder'] |
||||||
|
|
||||||
|
volume = database.session \ |
||||||
|
.query(database.Volumes) \ |
||||||
|
.filter(database.Volumes.volume_id == volume_id) \ |
||||||
|
.first() |
||||||
|
|
||||||
|
return name_scheme.format( |
||||||
|
volume_id = volume.volume_id, |
||||||
|
volume_name = volume.volume_name, |
||||||
|
volume_year = volume.volume_year, |
||||||
|
volume_publisher_id = volume.volume_publisher_id, |
||||||
|
volume_url = volume.volume_url, |
||||||
|
volume_slug = volume.volume_slug, |
||||||
|
volume_sort_title = volume.volume_sort_title, |
||||||
|
publisher_name = volume.publisher.publisher_name, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
def file_name_by_id(issue_id): |
||||||
|
|
||||||
|
# name_scheme = "{volume_name} - {issue_number:0>3}" |
||||||
|
name_scheme = stashrconfig['NAMING']['file'] |
||||||
|
|
||||||
|
issue = database.session \ |
||||||
|
.query(database.Issues) \ |
||||||
|
.filter(database.Issues.issue_id == issue_id) \ |
||||||
|
.first() |
||||||
|
|
||||||
|
return name_scheme.format( |
||||||
|
issue_id = issue.issue_id, |
||||||
|
volume_name = issue.volume.volume_name, |
||||||
|
issue_number = issue.issue_number, |
||||||
|
volume_id = issue.volume.volume_id, |
||||||
|
issue_name = issue.issue_name, |
||||||
|
release_date = issue.issue_release_date, |
||||||
|
description = issue.issue_description, |
||||||
|
volume_description = issue.volume.volume_description, |
||||||
|
volume_slug = issue.volume.volume_slug |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
def file_name_by_db(issue): |
||||||
|
|
||||||
|
# name_scheme = "{volume_name} - {issue_number:0>3}" |
||||||
|
name_scheme = stashrconfig['NAMING']['file'] |
||||||
|
|
||||||
|
return sanitize_filename( |
||||||
|
name_scheme.format( |
||||||
|
issue_id = issue.issue_id, |
||||||
|
volume_name = issue.volume.volume_name, |
||||||
|
issue_number = issue.issue_number, |
||||||
|
volume_id = issue.volume.volume_id, |
||||||
|
issue_name = issue.issue_name, |
||||||
|
release_date = issue.issue_release_date, |
||||||
|
description = issue.issue_description, |
||||||
|
volume_description = issue.volume.volume_description, |
||||||
|
volume_slug = issue.volume.volume_slug |
||||||
|
) |
||||||
|
) |
@ -0,0 +1,114 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Filename Parsing Functions |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import datetime, time, pathlib, os, re |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
|
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log, database |
||||||
|
from stashr.comicvine import cv |
||||||
|
from stashr.config import stashrconfig |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- FILENAME PARSER FUNCTIONS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
def repl(filename): |
||||||
|
return ' ' * len(filename.group()) |
||||||
|
|
||||||
|
|
||||||
|
def fixSpaces( string, remove_dashes=True ): |
||||||
|
if remove_dashes: |
||||||
|
placeholders = ['[-_]',' +'] |
||||||
|
else: |
||||||
|
placeholders = ['[_]',' +'] |
||||||
|
for ph in placeholders: |
||||||
|
string = re.sub(ph, repl, string ) |
||||||
|
return string #.strip() |
||||||
|
|
||||||
|
|
||||||
|
def getIssueNumber(filename): |
||||||
|
|
||||||
|
found = False |
||||||
|
issue = '' |
||||||
|
|
||||||
|
filename = pathlib.Path(filename).stem |
||||||
|
|
||||||
|
filename = filename.replace("+", " ") |
||||||
|
filename = filename.replace("-", " ") |
||||||
|
|
||||||
|
filename = re.sub( "\(.*?\)", repl, filename) |
||||||
|
filename = re.sub( "\[.*?\]", repl, filename) |
||||||
|
|
||||||
|
filename = fixSpaces(filename) |
||||||
|
|
||||||
|
filename = re.sub( "of [\d]+", repl, filename) |
||||||
|
|
||||||
|
word_list = [] |
||||||
|
|
||||||
|
for m in re.finditer("\S+", filename): |
||||||
|
word_list.append(m.group(0)) |
||||||
|
|
||||||
|
if len(word_list) == 1: |
||||||
|
return issue |
||||||
|
|
||||||
|
for w in reversed(word_list): |
||||||
|
if re.match("\#\d*", w[0]): |
||||||
|
found = True |
||||||
|
break |
||||||
|
|
||||||
|
if not found: |
||||||
|
w = word_list[-1] |
||||||
|
if re.match('\d*', w[0]): |
||||||
|
found = True |
||||||
|
|
||||||
|
if not found: |
||||||
|
for w in reversed(word_list): |
||||||
|
if re.match("\#\S+", w[0]): |
||||||
|
found = True |
||||||
|
break |
||||||
|
if found: |
||||||
|
issue = w |
||||||
|
if issue[0] == '#': |
||||||
|
issue = issue[1:] |
||||||
|
issue = issue.lstrip('0') or 0 |
||||||
|
|
||||||
|
return issue |
@ -0,0 +1,44 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Path Definitions |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- PATHS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
base_path = os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + os.sep + '..' + os.sep) |
||||||
|
db_path = os.path.join(base_path, 'database.db') |
@ -0,0 +1,715 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Flask Routing Definitions |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import math, os, json |
||||||
|
from natsort import natsorted |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log, database, forms, config, utils, folders, stashr |
||||||
|
from stashr.stashr import app, pm |
||||||
|
# from stashr.stashr import permission_admin, permission_reader |
||||||
|
from stashr.config import stashrconfig |
||||||
|
|
||||||
|
""" --- FLASK IMPORTS --- """ |
||||||
|
from flask import flash, redirect, url_for, render_template, current_app, send_from_directory, request, send_file, jsonify |
||||||
|
|
||||||
|
""" --- FLASK EXTENSION IMPORTS --- """ |
||||||
|
from flask_login import login_user, current_user, login_required, logout_user |
||||||
|
from flask_bcrypt import check_password_hash, generate_password_hash |
||||||
|
# from flask_principal import RoleNeed, Identity, AnonymousIdentity, identity_changed, identity_loaded |
||||||
|
from sqlalchemy import or_ |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- FLASK-PRINCIPAL IDENTITY LOADING |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" |
||||||
|
@identity_loaded.connect_via(app) |
||||||
|
def on_identity_loaded(sender, identity): |
||||||
|
|
||||||
|
try: |
||||||
|
if identity.id is not None: |
||||||
|
user = f'user_{identity.id}' |
||||||
|
identity.provides.add(RoleNeed(database.session.query(database.Users).get(int(identity.id)).role)) |
||||||
|
except: |
||||||
|
logout_user() |
||||||
|
identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity()) |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- USERS AND AUTHENTICATION |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
@app.login_manager.user_loader |
||||||
|
def load_user(user_id): |
||||||
|
return database.session.query(database.Users).filter(database.Users.id == int(user_id)).first() |
||||||
|
|
||||||
|
|
||||||
|
def verify_password(username, password): |
||||||
|
if check_password_hash(database.session.query(database.Users).filter( |
||||||
|
(database.Users.username == username) | (database.Users.email == username)).first().password, password): |
||||||
|
return True |
||||||
|
return False |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- ROUTES |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- PUBLIC PAGES --- """ |
||||||
|
|
||||||
|
""" |
||||||
|
@app.route('/samplepost', methods=['POST']) |
||||||
|
def test_post(): |
||||||
|
print('new post') |
||||||
|
data = request.json['data'] |
||||||
|
print(type(data)) |
||||||
|
for item in data: |
||||||
|
print('itemm') |
||||||
|
return 'POST PAGE' |
||||||
|
""" |
||||||
|
|
||||||
|
# INDEX |
||||||
|
@app.route('/', methods=['GET']) |
||||||
|
def index_page(): |
||||||
|
|
||||||
|
if stashrconfig['APP']['first_run']: |
||||||
|
flash('First Run') |
||||||
|
return redirect(url_for('first_run_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'index_page.html', |
||||||
|
title='Home', |
||||||
|
open_registration=stashrconfig['APP']['open_registration'] |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# LOGIN |
||||||
|
@app.route('/login', methods=['GET', 'POST']) |
||||||
|
def login_page(): |
||||||
|
|
||||||
|
if current_user.is_authenticated: |
||||||
|
flash('User Logged In', 'Info') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
login_form = forms.login_form() |
||||||
|
|
||||||
|
if login_form.validate_on_submit() and login_form.login_button.data: |
||||||
|
if verify_password(login_form.username.data, login_form.password.data): |
||||||
|
user = database.session \ |
||||||
|
.query(database.Users) \ |
||||||
|
.filter((database.Users.username == login_form.username.data)| |
||||||
|
(database.Users.email == login_form.username.data)) \ |
||||||
|
.first() |
||||||
|
login_user(user, remember=login_form.remember_me.data) |
||||||
|
# identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) |
||||||
|
flash('Logged In', 'success') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
else: |
||||||
|
flash('Incorrect Username or Password', 'error') |
||||||
|
return redirect(url_for('login_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'login_page.html', |
||||||
|
login_form=login_form, |
||||||
|
title='Login', |
||||||
|
open_registration=stashrconfig['APP']['open_registration'] |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# REGISTER |
||||||
|
@app.route('/register', methods=['GET', 'POST']) |
||||||
|
def register_page(): |
||||||
|
|
||||||
|
if not stashrconfig['APP']['open_registration']: |
||||||
|
flash('Registration Closed', 'error') |
||||||
|
return redirect(url_for('login_page')) |
||||||
|
|
||||||
|
registration_form = forms.registration_form() |
||||||
|
|
||||||
|
if registration_form.validate_on_submit() and registration_form.register_button.data: |
||||||
|
utils.register_new_user(registration_form) |
||||||
|
return redirect(url_for('login_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'register_page.html', |
||||||
|
registration_form=registration_form, |
||||||
|
title='Register', |
||||||
|
open_registration=stashrconfig['APP']['open_registration'] |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# FORGOT PASSWORD |
||||||
|
@app.route('/forgot', methods=['GET', 'POST']) |
||||||
|
def forgot_page(): |
||||||
|
|
||||||
|
if current_user.is_authenticated: |
||||||
|
flash('User Logged In', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
forgot_password_form = forms.forgot_password_form() |
||||||
|
|
||||||
|
if forgot_password_form.validate_on_submit() and forgot_password_form.forgot_button.data: |
||||||
|
# utils.send_password_change_request(forgot_password_form) |
||||||
|
flash('Email Reminder Sent', 'info') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'forgot_password_page.html', |
||||||
|
forgot_password_form=forgot_password_form, |
||||||
|
title='Forgot Password', |
||||||
|
open_registration=stashrconfig['APP']['open_registration'] |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- LOGOUT --- """ |
||||||
|
|
||||||
|
|
||||||
|
# LOGOUT |
||||||
|
@app.route('/logout') |
||||||
|
@login_required |
||||||
|
def logout_page(): |
||||||
|
logout_user() |
||||||
|
# identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity()) |
||||||
|
flash('Logged Out', 'info') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
|
||||||
|
""" --- SETTINGS --- """ |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS |
||||||
|
@app.route('/settings') |
||||||
|
@login_required |
||||||
|
def settings_page(section=None, user_id=None, user_function=None): |
||||||
|
|
||||||
|
# if not permission_admin.require().can(): |
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
return redirect(url_for('settings_app_page')) |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - APP |
||||||
|
@app.route('/settings/app', methods=['GET', 'POST']) |
||||||
|
def settings_app_page(): |
||||||
|
|
||||||
|
# if not permission_admin.require().can(): |
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
settings_form = forms.update_app_settings_form() |
||||||
|
|
||||||
|
if request.method == 'POST': |
||||||
|
if settings_form.validate(): |
||||||
|
utils.update_app_settings(settings_form) |
||||||
|
flash('App Settings Updated', 'success') |
||||||
|
else: |
||||||
|
print(settings_form.errors.items()) |
||||||
|
for error in settings_form.errors.items(): |
||||||
|
flash(f'{error[0]}: {error[1]}', 'error') |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'settings_page_app.html', |
||||||
|
title='App Settings', |
||||||
|
settings_form=settings_form |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - DIRECTORIES |
||||||
|
@app.route('/settings/directories', methods=['GET', 'POST']) |
||||||
|
def settings_directories_page(): |
||||||
|
|
||||||
|
directories_form = forms.update_directory_settings_form() |
||||||
|
|
||||||
|
if request.method == 'POST': |
||||||
|
if directories_form.validate(): |
||||||
|
utils.update_directory_settings(directories_form) |
||||||
|
flash('Directory Settings Updated', 'success') |
||||||
|
else: |
||||||
|
print(directories_form.errors.items()) |
||||||
|
for error in directories_form.errors.items(): |
||||||
|
flash(f'{error[0]}: {error[1]}', 'error') |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'settings_page_directories.html', |
||||||
|
title='Directory Settings', |
||||||
|
directories_form=directories_form |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - MAIL |
||||||
|
@app.route('/settings/mail', methods=['GET', 'POST']) |
||||||
|
def settings_mail_page(): |
||||||
|
|
||||||
|
# if not permission_admin.require().can(): |
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
mail_form = forms.update_mail_settings_form() |
||||||
|
|
||||||
|
if request.method == 'POST': |
||||||
|
if mail_form.validate(): |
||||||
|
utils.update_mail_settings(mail_form) |
||||||
|
flash('Mail Settings Updated', 'success') |
||||||
|
else: |
||||||
|
for error in mail_form.errors.items(): |
||||||
|
flash(f'{error[0]}: {error[1]}', 'error') |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'settings_page_mail.html', |
||||||
|
title='Mail Settings', |
||||||
|
mail_form=mail_form |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - TASKS |
||||||
|
@app.route('/settings/tasks', methods=['GET', 'POST']) |
||||||
|
def settings_tasks_page(): |
||||||
|
|
||||||
|
# if not permission_admin.require().can(): |
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'settings_page_tasks.html', |
||||||
|
title='Task Settings', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - ALL USERS |
||||||
|
@app.route('/settings/users', methods=['GET', 'POST']) |
||||||
|
def settings_all_users_page(): |
||||||
|
|
||||||
|
# if not permission_admin.require().can(): |
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
users = database.session.query(database.Users).all() |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'settings_page_all_users.html', |
||||||
|
title='Users', |
||||||
|
ratings_dict=database.ratings_dict, |
||||||
|
users=users, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - SINGLE USER |
||||||
|
@app.route('/settings/users/<user_id>', methods=['GET', 'POST']) |
||||||
|
def settings_single_user_page(user_id): |
||||||
|
""" |
||||||
|
if not permission_admin.require().can(): |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
if current_user.id != user_id: |
||||||
|
return redirect(url_for('settings_all_users_page')) |
||||||
|
""" |
||||||
|
|
||||||
|
user = database.session.query(database.Users).get(int(user_id)) |
||||||
|
|
||||||
|
change_password_form = forms.change_password_form(user) |
||||||
|
reset_password_form = forms.reset_password_form(user) |
||||||
|
delete_user_form = forms.delete_user_form(user) |
||||||
|
edit_user_form = forms.edit_user_form(user) |
||||||
|
|
||||||
|
if request.method == 'POST': |
||||||
|
if change_password_form.update_password_button.data: |
||||||
|
if change_password_form.validate(): |
||||||
|
utils.change_user_password(change_password_form) |
||||||
|
flash('Password Updated','success') |
||||||
|
else: |
||||||
|
for error in change_password_form.errors.items(): |
||||||
|
flash(f'{error[0]}: {error[1]}', 'error') |
||||||
|
if reset_password_form.reset_password_button.data: |
||||||
|
if reset_password_form.validate() and current_user.role == 'admin': |
||||||
|
utils.reset_user_password(reset_password_form) |
||||||
|
flash('Password Reset', 'success') |
||||||
|
else: |
||||||
|
for error in reset_password_form.errors.items(): |
||||||
|
flash(f'{error[0]}: {error[1]}', 'error') |
||||||
|
if delete_user_form.delete_user_button.data: |
||||||
|
if delete_user_form.validate() and current_user.role == 'admin': |
||||||
|
utils.delete_user_account(delete_user_form) |
||||||
|
flash('User Deleted', 'success') |
||||||
|
return redirect(url_for('settings_all_users_page')) |
||||||
|
else: |
||||||
|
flash(current_user.role,'error') |
||||||
|
if edit_user_form.edit_user_button.data: |
||||||
|
if edit_user_form.validate(): |
||||||
|
utils.edit_user_account(edit_user_form) |
||||||
|
flash('User Account Edited', 'success') |
||||||
|
else: |
||||||
|
for error in edit_user_form.errors.items(): |
||||||
|
flash(f'{error[0]}: {error[1]}', 'error') |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'settings_page_single_user.html', |
||||||
|
user=user, |
||||||
|
change_password_form=change_password_form, |
||||||
|
reset_password_form = reset_password_form, |
||||||
|
delete_user_form = delete_user_form, |
||||||
|
edit_user_form = edit_user_form, |
||||||
|
title='User Information', |
||||||
|
user_id=user_id |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - NEW USER |
||||||
|
@app.route('/settings/newuser', methods=['GET', 'POST']) |
||||||
|
def settings_new_user_page(): |
||||||
|
|
||||||
|
# if not permission_admin.require().can(): |
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
new_user_form = forms.new_user_form() |
||||||
|
|
||||||
|
if request.method == 'POST': |
||||||
|
if new_user_form.validate(): |
||||||
|
utils.create_new_user(new_user_form) |
||||||
|
flash('User Created', 'success') |
||||||
|
return redirect(url_for('settings_all_users_page')) |
||||||
|
else: |
||||||
|
for error in new_user_form.errors.items(): |
||||||
|
flash(f'{error[0]}: {error[1]}', 'error') |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'settings_page_new_user.html', |
||||||
|
title='New User', |
||||||
|
new_user_form=new_user_form |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - STATS |
||||||
|
@app.route('/settings/stats', methods=['GET', 'POST']) |
||||||
|
def settings_stats_page(): |
||||||
|
|
||||||
|
# if not permission_admin.require().can(): |
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
return 'STATS SETTINGS PAGE' |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - LOG |
||||||
|
@app.route('/settings/log', methods=['GET', 'POST']) |
||||||
|
def settings_log_page(): |
||||||
|
|
||||||
|
# if not permission_admin.require().can(): |
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
return 'LOG SETTINGS PAGE' |
||||||
|
|
||||||
|
|
||||||
|
# SETTINGS - PLUGINS |
||||||
|
@app.route('/settings/plugins', methods=['GET', 'POST']) |
||||||
|
def settings_plugins_page(): |
||||||
|
|
||||||
|
# if not permission_admin.require().can(): |
||||||
|
if current_user.role != 'admin': |
||||||
|
flash('Permission Denied', 'error') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
plugins = pm.get_all_plugins |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'settings_page_plugins.html', |
||||||
|
title='Plugins', |
||||||
|
plugins=plugins |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- FIRST RUN --- """ |
||||||
|
|
||||||
|
|
||||||
|
# FIRST RUN |
||||||
|
@app.route('/firstrun', methods=['GET', 'POST']) |
||||||
|
def first_run_page(): |
||||||
|
|
||||||
|
if current_user.is_authenticated: |
||||||
|
flash('App Configured', 'info') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
if not stashrconfig['APP']['first_run']: |
||||||
|
flash('App Configured', 'info') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
first_run_form = forms.app_first_run_form() |
||||||
|
|
||||||
|
if request.method == 'POST' and first_run_form.first_run_button.data: |
||||||
|
utils.complete_first_run(first_run_form) |
||||||
|
flash('Setup Complete', 'success') |
||||||
|
return redirect(url_for('index_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'first_run_page.html', |
||||||
|
title='Configuration', |
||||||
|
first_run_form=first_run_form, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- COMIC PAGES --- """ |
||||||
|
|
||||||
|
|
||||||
|
# ALL VOLUMES |
||||||
|
@app.route('/volumes', methods=['GET']) |
||||||
|
@login_required |
||||||
|
def all_volumes_page(): |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'all_volumes_page.html', |
||||||
|
title='All Volumes', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SINGLE VOLUME |
||||||
|
@app.route('/volumes/<volume_id>', methods=['GET']) |
||||||
|
@login_required |
||||||
|
def single_volume_page(volume_id): |
||||||
|
|
||||||
|
volume = database.session \ |
||||||
|
.query(database.Volumes) \ |
||||||
|
.filter(or_(database.Volumes.volume_id == volume_id, |
||||||
|
database.Volumes.volume_slug == volume_id)) \ |
||||||
|
.filter(database.Volumes.volume_age_rating <= current_user.rating_allowed) \ |
||||||
|
.first() |
||||||
|
|
||||||
|
if volume is None: |
||||||
|
flash('Volume Not Found', 'error') |
||||||
|
return redirect(url_for('all_volumes_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'single_volume_page.html', |
||||||
|
title=volume.volume_name, |
||||||
|
volume_id=volume.volume_id, |
||||||
|
volume_slug=volume.volume_slug |
||||||
|
) |
||||||
|
|
||||||
|
# ALL PUBLISHERS PAGE |
||||||
|
@app.route('/publishers') |
||||||
|
@login_required |
||||||
|
def all_publishers_page(): |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'all_publishers_page.html', |
||||||
|
title='All Publishers', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SINGLE PUBLISHERS PAGE |
||||||
|
@app.route('/publishers/<publisher_id>') |
||||||
|
@login_required |
||||||
|
def single_publisher_page(publisher_id): |
||||||
|
|
||||||
|
publisher = database.session \ |
||||||
|
.query(database.Publishers) \ |
||||||
|
.filter(database.Publishers.publisher_id == publisher_id) \ |
||||||
|
.first() |
||||||
|
|
||||||
|
if publisher is None: |
||||||
|
flash('Publisher Not Found', 'error') |
||||||
|
return redirect(url_for('all_publishers_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'single_publisher_page.html', |
||||||
|
title=publisher.publisher_name, |
||||||
|
publisher_id=publisher_id, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- READING COMIC PAGES --- """ |
||||||
|
|
||||||
|
|
||||||
|
# READ ISSUE PAGE |
||||||
|
@app.route('/read/<issue_id>') |
||||||
|
@login_required |
||||||
|
def read_issue_page(issue_id): |
||||||
|
|
||||||
|
issue = database.session \ |
||||||
|
.query(database.Issues) \ |
||||||
|
.filter(database.Issues.issue_id == issue_id) \ |
||||||
|
.first() |
||||||
|
|
||||||
|
print(issue.__dict__) |
||||||
|
|
||||||
|
if issue.volume.volume_age_rating > current_user.rating_allowed: |
||||||
|
flash('Access Denied', 'error') |
||||||
|
return redirect(url_for('all_volumes_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'read_issue_page.html', |
||||||
|
title='Read Issue', |
||||||
|
issue_id=issue_id, |
||||||
|
comic_name=f'{issue.volume.volume_name} - #{issue.issue_number}' |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- NEW RELEASES --- """ |
||||||
|
|
||||||
|
|
||||||
|
# NEW RELEASES |
||||||
|
@app.route('/releases') |
||||||
|
@login_required |
||||||
|
def new_releases_page(): |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'new_releases_page.html', |
||||||
|
title='New Releases', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- READING LIST --- """ |
||||||
|
|
||||||
|
|
||||||
|
# READING LIST |
||||||
|
@app.route('/readinglist') |
||||||
|
@login_required |
||||||
|
def reading_list_page(): |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'reading_list_page.html', |
||||||
|
title='Reading List', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- FOLDERS --- """ |
||||||
|
@app.route('/scrape') |
||||||
|
@login_required |
||||||
|
def scrape_folders_page(): |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'scrape_page.html', |
||||||
|
title="Scrape" |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- COLLECTIONS --- """ |
||||||
|
|
||||||
|
|
||||||
|
# ALL COLLECTIONS |
||||||
|
@app.route('/collections') |
||||||
|
@login_required |
||||||
|
def all_collections_page(): |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'all_collections_page.html', |
||||||
|
title='Collections', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# SINGLE COLLECTIONS |
||||||
|
@app.route('/collections/<collection_slug>', methods=['GET']) |
||||||
|
@login_required |
||||||
|
def single_collection_page(collection_slug): |
||||||
|
|
||||||
|
collection = database.session \ |
||||||
|
.query(database.Collections) \ |
||||||
|
.filter(database.Collections.collection_slug == collection_slug) \ |
||||||
|
.first() |
||||||
|
|
||||||
|
if collection is None: |
||||||
|
flash('Collection Not Found', 'error') |
||||||
|
return redirect(url_for('all_collections_page')) |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'single_collection_page.html', |
||||||
|
title=collection.collection_name, |
||||||
|
collection_slug = collection_slug, |
||||||
|
collection=collection, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- SEARCH --- """ |
||||||
|
|
||||||
|
|
||||||
|
# SEARCH PAGE |
||||||
|
@app.route('/search', methods=['GET']) |
||||||
|
@login_required |
||||||
|
def search_page(): |
||||||
|
|
||||||
|
return render_template( |
||||||
|
'search_page.html', |
||||||
|
title='Search', |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
""" --- CUSTOM ROUTING --- """ |
||||||
|
|
||||||
|
|
||||||
|
# CUSTOM STATIC COVER |
||||||
|
@app.route('/images/<foldername>/<path:filename>') |
||||||
|
@login_required |
||||||
|
def custom_image_static(foldername, filename): |
||||||
|
|
||||||
|
path = stashrconfig['DIRECTORY']['images'] |
||||||
|
|
||||||
|
return send_from_directory( |
||||||
|
os.path.join( |
||||||
|
# folders.base_path, |
||||||
|
folders.StashrPaths().base_path(), |
||||||
|
path, |
||||||
|
foldername |
||||||
|
), |
||||||
|
filename |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- DEVELOPMENT |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
if __name__=='__main__': |
||||||
|
logger.warning('Starting Development Server') |
||||||
|
app.run(host='0.0.0.0', port=5002, debug=True) |
@ -0,0 +1,105 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Server Start |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os, signal |
||||||
|
from socket import error as SocketError |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr.stashr import app |
||||||
|
from stashr import log |
||||||
|
from stashr import database, routes |
||||||
|
from stashr.config import stashrconfig |
||||||
|
|
||||||
|
""" --- GEVENT IMPORT --- """ |
||||||
|
from gevent.pywsgi import WSGIServer |
||||||
|
from gevent.pool import Pool |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- SERVER |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
class Server: |
||||||
|
|
||||||
|
wsgiserver = None |
||||||
|
restart = False |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
try: |
||||||
|
signal.signal(signal.SIGHUP, self.restart_server) |
||||||
|
except: |
||||||
|
signal.signal(signal.SIGINT, self.restart_server) |
||||||
|
pass |
||||||
|
|
||||||
|
def define_wsgi(self): |
||||||
|
try: |
||||||
|
ssl_args = dict() |
||||||
|
if os.name == 'nt': |
||||||
|
app.logger.debug('NT') |
||||||
|
app.logger.debug('PORT: {0}'.format(stashrconfig['APP']['server_port'])) |
||||||
|
self.wsgiserver = WSGIServer(('0.0.0.0', int(stashrconfig['APP']['server_port'])), app, spawn=Pool(), **ssl_args) |
||||||
|
else: |
||||||
|
app.logger.debug('NOT NT') |
||||||
|
app.logger.debug('PORT: {0}'.format(stashrconfig['APP']['server_port'])) |
||||||
|
self.wsgiserver = WSGIServer(('', int(stashrconfig['APP']['server_port'])), app, spawn=Pool(), **ssl_args) |
||||||
|
except SocketError: |
||||||
|
app.logger.debug('PORT: {0}'.format(stashrconfig['APP']['server_port'])) |
||||||
|
self.wsgiserver = WSGIServer(('0.0.0.0', int(stashrconfig['APP']['server_port'])), app, spawn=Pool(), **ssl_args) |
||||||
|
except Exception: |
||||||
|
app.logger.debug('Unknown Error while starting gevent') |
||||||
|
|
||||||
|
def start_server(self): |
||||||
|
app.logger.debug('STARTING SERVER') |
||||||
|
self.define_wsgi() |
||||||
|
self.wsgiserver.serve_forever() |
||||||
|
|
||||||
|
def stop_server(self): |
||||||
|
app.logger.debug('STOPPING SERVER') |
||||||
|
self.wsgiserver.stop() |
||||||
|
|
||||||
|
def restart_server(self, ignored_signum, ignored_frame): |
||||||
|
app.logger.debug('Restarting Server') |
||||||
|
print('restatr') |
||||||
|
self.wsgiserver.close() |
||||||
|
# self.define_wsgi() |
||||||
|
self.wsgiserver.start() |
||||||
|
|
||||||
|
|
||||||
|
server = Server() |
@ -0,0 +1,170 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - App Definitions |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
from huey import SqliteHuey |
||||||
|
huey = SqliteHuey() |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
import os |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import paths, log, comicvine, config, api |
||||||
|
from stashr import tasks |
||||||
|
|
||||||
|
""" --- FLASK IMPORT --- """ |
||||||
|
from flask import Flask |
||||||
|
|
||||||
|
""" --- FLASK EXTENSION IMPORTS --- """ |
||||||
|
from itsdangerous import URLSafeTimedSerializer |
||||||
|
|
||||||
|
from flask_login import LoginManager |
||||||
|
# from flask_principal import Principal, Permission, RoleNeed |
||||||
|
from flask_mail import Mail |
||||||
|
from flask_pluginkit import PluginManager |
||||||
|
from flask.signals import Namespace |
||||||
|
from flask_cors import CORS |
||||||
|
|
||||||
|
from flasgger import Swagger, APISpec |
||||||
|
# from apispec.ext.marshmallow import MarshmallowPlugin |
||||||
|
|
||||||
|
""" --- CONFIG IMPORTS --- """ |
||||||
|
from stashr.config import stashrconfig |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- SETUP |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
""" --- CREATE SIGNALS --- """ |
||||||
|
namespace = Namespace() |
||||||
|
stashr_notification = namespace.signal('stashr_notification') |
||||||
|
stashr_new_releases_update = namespace.signal('stashr_new_releases_update') |
||||||
|
|
||||||
|
""" --- CREATE READING LIST --- """ |
||||||
|
# reading_image_list = [] |
||||||
|
|
||||||
|
""" --- GET CONFIGURATION --- """ |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- CONFIGURATION |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
class FlaskConfig(object): |
||||||
|
|
||||||
|
# FLASK SETTINGS |
||||||
|
DEBUG = False |
||||||
|
SECRET_KEY = stashrconfig['SECURITY']['cookie_secret'] |
||||||
|
CSRF_ENABLED = True |
||||||
|
|
||||||
|
JSON_SORT_KEYS = False |
||||||
|
|
||||||
|
# FLASK MAIL SETTINGS |
||||||
|
MAIL_SERVER = stashrconfig['MAIL']['mail_server'] |
||||||
|
MAIL_PORT = stashrconfig['MAIL']['mail_port'] |
||||||
|
MAIL_USE_SSL = stashrconfig['MAIL']['mail_use_ssl'] |
||||||
|
MAIL_USE_TLS = False |
||||||
|
MAIL_USERNAME = stashrconfig['MAIL']['mail_username'] |
||||||
|
MAIL_PASSWORD = stashrconfig['MAIL']['mail_password'] |
||||||
|
MAIL_DEFAULT_SENDER = 'Stashr Admin' |
||||||
|
|
||||||
|
# FLASK PRINCIPAL |
||||||
|
# SKIP_STATIC = True |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- CONFIGURATION |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- CREATE FLASK APP --- """ |
||||||
|
app = Flask(__name__) |
||||||
|
|
||||||
|
""" --- REGISTER API BLUEPRINT --- """ |
||||||
|
app.register_blueprint(api.api, url_prefix='/api') |
||||||
|
|
||||||
|
""" --- CONFIGURE FLASK --- """ |
||||||
|
app.config.from_object(__name__+'.FlaskConfig') |
||||||
|
|
||||||
|
template = { |
||||||
|
"swagger": "2.0", |
||||||
|
"info": { |
||||||
|
"title": "Stashr", |
||||||
|
"description": "API to access Stashr Installations", |
||||||
|
"version": "0.0.1" |
||||||
|
}, |
||||||
|
"basePath": "/api", # base bash for blueprint registration |
||||||
|
"schemes": [ |
||||||
|
"http", |
||||||
|
"https" |
||||||
|
], |
||||||
|
"operationId": "getmyData" |
||||||
|
} |
||||||
|
|
||||||
|
swagger = Swagger(app, template=template) |
||||||
|
|
||||||
|
""" --- INITIALIZE FLASK EXTENSIONS --- """ |
||||||
|
|
||||||
|
|
||||||
|
# ITSDANGEROUS |
||||||
|
ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) |
||||||
|
EMAIL_CONFIRM_KEY = stashrconfig['SECURITY']['cookie_secret'] |
||||||
|
|
||||||
|
# FLASK_LOGIN |
||||||
|
login_manager = LoginManager() |
||||||
|
login_manager.init_app(app) |
||||||
|
login_manager.login_view = "login_page" |
||||||
|
|
||||||
|
# INITIALIZE FLASK PRINCIPAL |
||||||
|
# principals = Principal(app, skip_static=True) |
||||||
|
# permission_admin = Permission(RoleNeed('admin')) |
||||||
|
# permission_reader = Permission(RoleNeed('reader')) |
||||||
|
|
||||||
|
# FLASK MAIL |
||||||
|
mail = Mail(app) |
||||||
|
|
||||||
|
# FLASK CORS |
||||||
|
cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) |
||||||
|
|
||||||
|
# PLUGIN MANAGER |
||||||
|
plugin_folder = os.path.join( |
||||||
|
paths.base_path, |
||||||
|
stashrconfig['DIRECTORY']['plugins'] |
||||||
|
) |
||||||
|
|
||||||
|
pm = PluginManager(app, plugins_base=paths.base_path, plugins_folder=stashrconfig['DIRECTORY']['plugins']) |
After Width: | Height: | Size: 125 KiB |
After Width: | Height: | Size: 693 B |
After Width: | Height: | Size: 173 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1,428 @@ |
|||||||
|
/*! |
||||||
|
* Bootstrap Reboot v5.0.0-beta2 (https://getbootstrap.com/) |
||||||
|
* Copyright 2011-2021 The Bootstrap Authors |
||||||
|
* Copyright 2011-2021 Twitter, Inc. |
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) |
||||||
|
*/ |
||||||
|
*, |
||||||
|
*::before, |
||||||
|
*::after { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) { |
||||||
|
:root { |
||||||
|
scroll-behavior: smooth; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; |
||||||
|
font-size: 1rem; |
||||||
|
font-weight: 400; |
||||||
|
line-height: 1.5; |
||||||
|
color: #212529; |
||||||
|
background-color: #fff; |
||||||
|
-webkit-text-size-adjust: 100%; |
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
[tabindex="-1"]:focus:not(:focus-visible) { |
||||||
|
outline: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
hr { |
||||||
|
margin: 1rem 0; |
||||||
|
color: inherit; |
||||||
|
background-color: currentColor; |
||||||
|
border: 0; |
||||||
|
opacity: 0.25; |
||||||
|
} |
||||||
|
|
||||||
|
hr:not([size]) { |
||||||
|
height: 1px; |
||||||
|
} |
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-weight: 500; |
||||||
|
line-height: 1.2; |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: calc(1.375rem + 1.5vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h1 { |
||||||
|
font-size: 2.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: calc(1.325rem + 0.9vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h2 { |
||||||
|
font-size: 2rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: calc(1.3rem + 0.6vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h3 { |
||||||
|
font-size: 1.75rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h4 { |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h4 { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h5 { |
||||||
|
font-size: 1.25rem; |
||||||
|
} |
||||||
|
|
||||||
|
h6 { |
||||||
|
font-size: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
abbr[title], |
||||||
|
abbr[data-bs-original-title] { |
||||||
|
text-decoration: underline; |
||||||
|
-webkit-text-decoration: underline dotted; |
||||||
|
text-decoration: underline dotted; |
||||||
|
cursor: help; |
||||||
|
-webkit-text-decoration-skip-ink: none; |
||||||
|
text-decoration-skip-ink: none; |
||||||
|
} |
||||||
|
|
||||||
|
address { |
||||||
|
margin-bottom: 1rem; |
||||||
|
font-style: normal; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul { |
||||||
|
padding-left: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul, |
||||||
|
dl { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol ol, |
||||||
|
ul ul, |
||||||
|
ol ul, |
||||||
|
ul ol { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
dt { |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
dd { |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
margin-left: 0; |
||||||
|
} |
||||||
|
|
||||||
|
blockquote { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
b, |
||||||
|
strong { |
||||||
|
font-weight: bolder; |
||||||
|
} |
||||||
|
|
||||||
|
small { |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
|
||||||
|
mark { |
||||||
|
padding: 0.2em; |
||||||
|
background-color: #fcf8e3; |
||||||
|
} |
||||||
|
|
||||||
|
sub, |
||||||
|
sup { |
||||||
|
position: relative; |
||||||
|
font-size: 0.75em; |
||||||
|
line-height: 0; |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
sub { |
||||||
|
bottom: -0.25em; |
||||||
|
} |
||||||
|
|
||||||
|
sup { |
||||||
|
top: -0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: #0d6efd; |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
a:hover { |
||||||
|
color: #0a58ca; |
||||||
|
} |
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
pre, |
||||||
|
code, |
||||||
|
kbd, |
||||||
|
samp { |
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
||||||
|
font-size: 1em; |
||||||
|
direction: ltr /* rtl:ignore */; |
||||||
|
unicode-bidi: bidi-override; |
||||||
|
} |
||||||
|
|
||||||
|
pre { |
||||||
|
display: block; |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
overflow: auto; |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
pre code { |
||||||
|
font-size: inherit; |
||||||
|
color: inherit; |
||||||
|
word-break: normal; |
||||||
|
} |
||||||
|
|
||||||
|
code { |
||||||
|
font-size: 0.875em; |
||||||
|
color: #d63384; |
||||||
|
word-wrap: break-word; |
||||||
|
} |
||||||
|
a > code { |
||||||
|
color: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
kbd { |
||||||
|
padding: 0.2rem 0.4rem; |
||||||
|
font-size: 0.875em; |
||||||
|
color: #fff; |
||||||
|
background-color: #212529; |
||||||
|
border-radius: 0.2rem; |
||||||
|
} |
||||||
|
kbd kbd { |
||||||
|
padding: 0; |
||||||
|
font-size: 1em; |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
figure { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
img, |
||||||
|
svg { |
||||||
|
vertical-align: middle; |
||||||
|
} |
||||||
|
|
||||||
|
table { |
||||||
|
caption-side: bottom; |
||||||
|
border-collapse: collapse; |
||||||
|
} |
||||||
|
|
||||||
|
caption { |
||||||
|
padding-top: 0.5rem; |
||||||
|
padding-bottom: 0.5rem; |
||||||
|
color: #6c757d; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
|
||||||
|
th { |
||||||
|
text-align: inherit; |
||||||
|
text-align: -webkit-match-parent; |
||||||
|
} |
||||||
|
|
||||||
|
thead, |
||||||
|
tbody, |
||||||
|
tfoot, |
||||||
|
tr, |
||||||
|
td, |
||||||
|
th { |
||||||
|
border-color: inherit; |
||||||
|
border-style: solid; |
||||||
|
border-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
label { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
border-radius: 0; |
||||||
|
} |
||||||
|
|
||||||
|
button:focus:not(:focus-visible) { |
||||||
|
outline: 0; |
||||||
|
} |
||||||
|
|
||||||
|
input, |
||||||
|
button, |
||||||
|
select, |
||||||
|
optgroup, |
||||||
|
textarea { |
||||||
|
margin: 0; |
||||||
|
font-family: inherit; |
||||||
|
font-size: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
select { |
||||||
|
text-transform: none; |
||||||
|
} |
||||||
|
|
||||||
|
[role=button] { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
word-wrap: normal; |
||||||
|
} |
||||||
|
|
||||||
|
[list]::-webkit-calendar-picker-indicator { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
[type=button], |
||||||
|
[type=reset], |
||||||
|
[type=submit] { |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
button:not(:disabled), |
||||||
|
[type=button]:not(:disabled), |
||||||
|
[type=reset]:not(:disabled), |
||||||
|
[type=submit]:not(:disabled) { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
::-moz-focus-inner { |
||||||
|
padding: 0; |
||||||
|
border-style: none; |
||||||
|
} |
||||||
|
|
||||||
|
textarea { |
||||||
|
resize: vertical; |
||||||
|
} |
||||||
|
|
||||||
|
fieldset { |
||||||
|
min-width: 0; |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
legend { |
||||||
|
float: left; |
||||||
|
width: 100%; |
||||||
|
padding: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
legend { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
legend + * { |
||||||
|
clear: left; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper, |
||||||
|
::-webkit-datetime-edit-text, |
||||||
|
::-webkit-datetime-edit-minute, |
||||||
|
::-webkit-datetime-edit-hour-field, |
||||||
|
::-webkit-datetime-edit-day-field, |
||||||
|
::-webkit-datetime-edit-month-field, |
||||||
|
::-webkit-datetime-edit-year-field { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-inner-spin-button { |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
[type=search] { |
||||||
|
outline-offset: -2px; |
||||||
|
-webkit-appearance: textfield; |
||||||
|
} |
||||||
|
|
||||||
|
/* rtl:raw: |
||||||
|
[type="tel"], |
||||||
|
[type="url"], |
||||||
|
[type="email"], |
||||||
|
[type="number"] { |
||||||
|
direction: ltr; |
||||||
|
} |
||||||
|
*/ |
||||||
|
::-webkit-search-decoration { |
||||||
|
-webkit-appearance: none; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::file-selector-button { |
||||||
|
font: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-file-upload-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
output { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
iframe { |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
summary { |
||||||
|
display: list-item; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
progress { |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
[hidden] { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.css.map */ |
@ -0,0 +1,425 @@ |
|||||||
|
/*! |
||||||
|
* Bootstrap Reboot v5.0.0-beta2 (https://getbootstrap.com/) |
||||||
|
* Copyright 2011-2021 The Bootstrap Authors |
||||||
|
* Copyright 2011-2021 Twitter, Inc. |
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) |
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) |
||||||
|
*/ |
||||||
|
*, |
||||||
|
*::before, |
||||||
|
*::after { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) { |
||||||
|
:root { |
||||||
|
scroll-behavior: smooth; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; |
||||||
|
font-size: 1rem; |
||||||
|
font-weight: 400; |
||||||
|
line-height: 1.5; |
||||||
|
color: #212529; |
||||||
|
background-color: #fff; |
||||||
|
-webkit-text-size-adjust: 100%; |
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
[tabindex="-1"]:focus:not(:focus-visible) { |
||||||
|
outline: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
hr { |
||||||
|
margin: 1rem 0; |
||||||
|
color: inherit; |
||||||
|
background-color: currentColor; |
||||||
|
border: 0; |
||||||
|
opacity: 0.25; |
||||||
|
} |
||||||
|
|
||||||
|
hr:not([size]) { |
||||||
|
height: 1px; |
||||||
|
} |
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-weight: 500; |
||||||
|
line-height: 1.2; |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: calc(1.375rem + 1.5vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h1 { |
||||||
|
font-size: 2.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: calc(1.325rem + 0.9vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h2 { |
||||||
|
font-size: 2rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: calc(1.3rem + 0.6vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h3 { |
||||||
|
font-size: 1.75rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h4 { |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
h4 { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
h5 { |
||||||
|
font-size: 1.25rem; |
||||||
|
} |
||||||
|
|
||||||
|
h6 { |
||||||
|
font-size: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
p { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
abbr[title], |
||||||
|
abbr[data-bs-original-title] { |
||||||
|
text-decoration: underline; |
||||||
|
-webkit-text-decoration: underline dotted; |
||||||
|
text-decoration: underline dotted; |
||||||
|
cursor: help; |
||||||
|
-webkit-text-decoration-skip-ink: none; |
||||||
|
text-decoration-skip-ink: none; |
||||||
|
} |
||||||
|
|
||||||
|
address { |
||||||
|
margin-bottom: 1rem; |
||||||
|
font-style: normal; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul { |
||||||
|
padding-right: 2rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol, |
||||||
|
ul, |
||||||
|
dl { |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
ol ol, |
||||||
|
ul ul, |
||||||
|
ol ul, |
||||||
|
ul ol { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
dt { |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
dd { |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
margin-right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
blockquote { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
b, |
||||||
|
strong { |
||||||
|
font-weight: bolder; |
||||||
|
} |
||||||
|
|
||||||
|
small { |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
|
||||||
|
mark { |
||||||
|
padding: 0.2em; |
||||||
|
background-color: #fcf8e3; |
||||||
|
} |
||||||
|
|
||||||
|
sub, |
||||||
|
sup { |
||||||
|
position: relative; |
||||||
|
font-size: 0.75em; |
||||||
|
line-height: 0; |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
sub { |
||||||
|
bottom: -0.25em; |
||||||
|
} |
||||||
|
|
||||||
|
sup { |
||||||
|
top: -0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: #0d6efd; |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
a:hover { |
||||||
|
color: #0a58ca; |
||||||
|
} |
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover { |
||||||
|
color: inherit; |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
pre, |
||||||
|
code, |
||||||
|
kbd, |
||||||
|
samp { |
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
||||||
|
font-size: 1em; |
||||||
|
direction: ltr ; |
||||||
|
unicode-bidi: bidi-override; |
||||||
|
} |
||||||
|
|
||||||
|
pre { |
||||||
|
display: block; |
||||||
|
margin-top: 0; |
||||||
|
margin-bottom: 1rem; |
||||||
|
overflow: auto; |
||||||
|
font-size: 0.875em; |
||||||
|
} |
||||||
|
pre code { |
||||||
|
font-size: inherit; |
||||||
|
color: inherit; |
||||||
|
word-break: normal; |
||||||
|
} |
||||||
|
|
||||||
|
code { |
||||||
|
font-size: 0.875em; |
||||||
|
color: #d63384; |
||||||
|
word-wrap: break-word; |
||||||
|
} |
||||||
|
a > code { |
||||||
|
color: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
kbd { |
||||||
|
padding: 0.2rem 0.4rem; |
||||||
|
font-size: 0.875em; |
||||||
|
color: #fff; |
||||||
|
background-color: #212529; |
||||||
|
border-radius: 0.2rem; |
||||||
|
} |
||||||
|
kbd kbd { |
||||||
|
padding: 0; |
||||||
|
font-size: 1em; |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
figure { |
||||||
|
margin: 0 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
img, |
||||||
|
svg { |
||||||
|
vertical-align: middle; |
||||||
|
} |
||||||
|
|
||||||
|
table { |
||||||
|
caption-side: bottom; |
||||||
|
border-collapse: collapse; |
||||||
|
} |
||||||
|
|
||||||
|
caption { |
||||||
|
padding-top: 0.5rem; |
||||||
|
padding-bottom: 0.5rem; |
||||||
|
color: #6c757d; |
||||||
|
text-align: right; |
||||||
|
} |
||||||
|
|
||||||
|
th { |
||||||
|
text-align: inherit; |
||||||
|
text-align: -webkit-match-parent; |
||||||
|
} |
||||||
|
|
||||||
|
thead, |
||||||
|
tbody, |
||||||
|
tfoot, |
||||||
|
tr, |
||||||
|
td, |
||||||
|
th { |
||||||
|
border-color: inherit; |
||||||
|
border-style: solid; |
||||||
|
border-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
label { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
border-radius: 0; |
||||||
|
} |
||||||
|
|
||||||
|
button:focus:not(:focus-visible) { |
||||||
|
outline: 0; |
||||||
|
} |
||||||
|
|
||||||
|
input, |
||||||
|
button, |
||||||
|
select, |
||||||
|
optgroup, |
||||||
|
textarea { |
||||||
|
margin: 0; |
||||||
|
font-family: inherit; |
||||||
|
font-size: inherit; |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
select { |
||||||
|
text-transform: none; |
||||||
|
} |
||||||
|
|
||||||
|
[role=button] { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
word-wrap: normal; |
||||||
|
} |
||||||
|
|
||||||
|
[list]::-webkit-calendar-picker-indicator { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
button, |
||||||
|
[type=button], |
||||||
|
[type=reset], |
||||||
|
[type=submit] { |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
button:not(:disabled), |
||||||
|
[type=button]:not(:disabled), |
||||||
|
[type=reset]:not(:disabled), |
||||||
|
[type=submit]:not(:disabled) { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
::-moz-focus-inner { |
||||||
|
padding: 0; |
||||||
|
border-style: none; |
||||||
|
} |
||||||
|
|
||||||
|
textarea { |
||||||
|
resize: vertical; |
||||||
|
} |
||||||
|
|
||||||
|
fieldset { |
||||||
|
min-width: 0; |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
legend { |
||||||
|
float: right; |
||||||
|
width: 100%; |
||||||
|
padding: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
font-size: calc(1.275rem + 0.3vw); |
||||||
|
line-height: inherit; |
||||||
|
} |
||||||
|
@media (min-width: 1200px) { |
||||||
|
legend { |
||||||
|
font-size: 1.5rem; |
||||||
|
} |
||||||
|
} |
||||||
|
legend + * { |
||||||
|
clear: right; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper, |
||||||
|
::-webkit-datetime-edit-text, |
||||||
|
::-webkit-datetime-edit-minute, |
||||||
|
::-webkit-datetime-edit-hour-field, |
||||||
|
::-webkit-datetime-edit-day-field, |
||||||
|
::-webkit-datetime-edit-month-field, |
||||||
|
::-webkit-datetime-edit-year-field { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-inner-spin-button { |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
[type=search] { |
||||||
|
outline-offset: -2px; |
||||||
|
-webkit-appearance: textfield; |
||||||
|
} |
||||||
|
|
||||||
|
[type="tel"], |
||||||
|
[type="url"], |
||||||
|
[type="email"], |
||||||
|
[type="number"] { |
||||||
|
direction: ltr; |
||||||
|
} |
||||||
|
::-webkit-search-decoration { |
||||||
|
-webkit-appearance: none; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper { |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
::file-selector-button { |
||||||
|
font: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-file-upload-button { |
||||||
|
font: inherit; |
||||||
|
-webkit-appearance: button; |
||||||
|
} |
||||||
|
|
||||||
|
output { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
iframe { |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
|
||||||
|
summary { |
||||||
|
display: list-item; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
progress { |
||||||
|
vertical-align: baseline; |
||||||
|
} |
||||||
|
|
||||||
|
[hidden] { |
||||||
|
display: none !important; |
||||||
|
} |
||||||
|
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ |
@ -0,0 +1,15 @@ |
|||||||
|
/*! |
||||||
|
* Font Awesome Free 5.15.2 by @fontawesome - https://fontawesome.com |
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) |
||||||
|
*/ |
||||||
|
@font-face { |
||||||
|
font-family: 'Font Awesome 5 Brands'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 400; |
||||||
|
font-display: block; |
||||||
|
src: url("../webfonts/fa-brands-400.eot"); |
||||||
|
src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } |
||||||
|
|
||||||
|
.fab { |
||||||
|
font-family: 'Font Awesome 5 Brands'; |
||||||
|
font-weight: 400; } |
@ -0,0 +1,15 @@ |
|||||||
|
/*! |
||||||
|
* Font Awesome Free 5.15.2 by @fontawesome - https://fontawesome.com |
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) |
||||||
|
*/ |
||||||
|
@font-face { |
||||||
|
font-family: 'Font Awesome 5 Free'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 400; |
||||||
|
font-display: block; |
||||||
|
src: url("../webfonts/fa-regular-400.eot"); |
||||||
|
src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } |
||||||
|
|
||||||
|
.far { |
||||||
|
font-family: 'Font Awesome 5 Free'; |
||||||
|
font-weight: 400; } |
@ -0,0 +1,16 @@ |
|||||||
|
/*! |
||||||
|
* Font Awesome Free 5.15.2 by @fontawesome - https://fontawesome.com |
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) |
||||||
|
*/ |
||||||
|
@font-face { |
||||||
|
font-family: 'Font Awesome 5 Free'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 900; |
||||||
|
font-display: block; |
||||||
|
src: url("../webfonts/fa-solid-900.eot"); |
||||||
|
src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } |
||||||
|
|
||||||
|
.fa, |
||||||
|
.fas { |
||||||
|
font-family: 'Font Awesome 5 Free'; |
||||||
|
font-weight: 900; } |
@ -0,0 +1,3 @@ |
|||||||
|
body { margin:0;background-color:black; } |
||||||
|
|
||||||
|
.stashr-button_container {position:fixed;bottom:0;right:0;opacity:.5;z-index:999;} |
@ -0,0 +1,72 @@ |
|||||||
|
/* START NEW STASHR CLASSES */ |
||||||
|
|
||||||
|
body { background:#444444; } |
||||||
|
ul { list-style-type: none; } |
||||||
|
.center { text-align:center; } |
||||||
|
|
||||||
|
@media screen and (max-width: 800px) { |
||||||
|
.stashr-cover_size {max-width:100px;min-width:20px;min-height:150px;} |
||||||
|
} |
||||||
|
@media screen and (min-width: 800px) { |
||||||
|
.stashr-cover_size {max-width:160px;min-width:100px;min-height:150px;} |
||||||
|
} |
||||||
|
|
||||||
|
#app {margin:0;padding:0;} |
||||||
|
|
||||||
|
body .stashrRead { margin:0;background-color:black; } |
||||||
|
|
||||||
|
.stashr-project_title { font-family: 'Grand Hotel', cursive;font-size:xx-large;line-height:40px;vertical-align: middle; } |
||||||
|
.stashr-series_title { font-family: 'Oswald', sans-serif;font-weight:bold; } |
||||||
|
|
||||||
|
.stashr-series_info {min-width:30%;max-width:60%;} |
||||||
|
|
||||||
|
.stashr-poster_wrapper {position:relative;background-color:pink;z-index:5;} |
||||||
|
.stashr-badge_tl {position:absolute;top:-5;left:-5;z-index:4;} |
||||||
|
.stashr-badge_tr {position:absolute;top:-5;right:-5;z-index:4;} |
||||||
|
.stashr-badge_bl {position:absolute;bottom:-5;left:-5;z-index:4;} |
||||||
|
.stashr-badge_br {position:absolute;bottom:-5;right:-5;z-index:4;} |
||||||
|
.stashr-poster_container {position:relative;overflow:hidden;} |
||||||
|
.stashr-overlay_top {position:absolute;top:0;background-color:rgba(220,220,220,.9);z-index:5;} |
||||||
|
.stashr-overlay_bottom {position:absolute;bottom:0;background-color:rgba(220,220,220,.9);z-index:5;} |
||||||
|
.stashr-poster_background {z-index:0;} |
||||||
|
.stashr-poster_link {position:absolute;top:0;left:0;width:100%;height:100%;} |
||||||
|
.stashr-poster_image {margin: 0 auto;display: block;vertical-align: middle;} |
||||||
|
|
||||||
|
.stashr-test_display {position:relative;display:inline-block;vertical-align:middle;} |
||||||
|
.stashr-test-image {margin:0 auto;display:block;vertical-align:middle;width:100%;} |
||||||
|
|
||||||
|
.swiper-container { width:100%;height:100%;padding:0px;margin:0px; } |
||||||
|
.swiper-wrapper { padding:0px;margin:0px; } |
||||||
|
.swiper-slide { width:100%;height:100%;background: black; } |
||||||
|
.swiper-slide img { height:100%; } |
||||||
|
|
||||||
|
.stashr-button_container {position:fixed;bottom:10;right:10;opacity:.7;z-index:999;} |
||||||
|
.stashr-button_container_reader {position:fixed;bottom:0;right:0;opacity:.7;z-index:999;} |
||||||
|
.stashr-button { display:table-cell;vertical-align:middle;width:60px;height:60px; } |
||||||
|
|
||||||
|
.stashr-check_box {position:absolute;top:0;right:0;z-index:5;} |
||||||
|
|
||||||
|
.btn-circle.btn-sm { |
||||||
|
width: 30px; |
||||||
|
height: 30px; |
||||||
|
padding: 6px 0px; |
||||||
|
border-radius: 15px; |
||||||
|
font-size: 8px; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
.btn-circle.btn-md { |
||||||
|
width: 50px; |
||||||
|
height: 50px; |
||||||
|
padding: 7px 10px; |
||||||
|
border-radius: 25px; |
||||||
|
font-size: 10px; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
.btn-circle.btn-xl { |
||||||
|
width: 70px; |
||||||
|
height: 70px; |
||||||
|
padding: 10px 16px; |
||||||
|
border-radius: 35px; |
||||||
|
font-size: 12px; |
||||||
|
text-align: center; |
||||||
|
} |
@ -0,0 +1,371 @@ |
|||||||
|
/*! |
||||||
|
* Font Awesome Free 5.15.2 by @fontawesome - https://fontawesome.com |
||||||
|
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) |
||||||
|
*/ |
||||||
|
svg:not(:root).svg-inline--fa { |
||||||
|
overflow: visible; } |
||||||
|
|
||||||
|
.svg-inline--fa { |
||||||
|
display: inline-block; |
||||||
|
font-size: inherit; |
||||||
|
height: 1em; |
||||||
|
overflow: visible; |
||||||
|
vertical-align: -.125em; } |
||||||
|
.svg-inline--fa.fa-lg { |
||||||
|
vertical-align: -.225em; } |
||||||
|
.svg-inline--fa.fa-w-1 { |
||||||
|
width: 0.0625em; } |
||||||
|
.svg-inline--fa.fa-w-2 { |
||||||
|
width: 0.125em; } |
||||||
|
.svg-inline--fa.fa-w-3 { |
||||||
|
width: 0.1875em; } |
||||||
|
.svg-inline--fa.fa-w-4 { |
||||||
|
width: 0.25em; } |
||||||
|
.svg-inline--fa.fa-w-5 { |
||||||
|
width: 0.3125em; } |
||||||
|
.svg-inline--fa.fa-w-6 { |
||||||
|
width: 0.375em; } |
||||||
|
.svg-inline--fa.fa-w-7 { |
||||||
|
width: 0.4375em; } |
||||||
|
.svg-inline--fa.fa-w-8 { |
||||||
|
width: 0.5em; } |
||||||
|
.svg-inline--fa.fa-w-9 { |
||||||
|
width: 0.5625em; } |
||||||
|
.svg-inline--fa.fa-w-10 { |
||||||
|
width: 0.625em; } |
||||||
|
.svg-inline--fa.fa-w-11 { |
||||||
|
width: 0.6875em; } |
||||||
|
.svg-inline--fa.fa-w-12 { |
||||||
|
width: 0.75em; } |
||||||
|
.svg-inline--fa.fa-w-13 { |
||||||
|
width: 0.8125em; } |
||||||
|
.svg-inline--fa.fa-w-14 { |
||||||
|
width: 0.875em; } |
||||||
|
.svg-inline--fa.fa-w-15 { |
||||||
|
width: 0.9375em; } |
||||||
|
.svg-inline--fa.fa-w-16 { |
||||||
|
width: 1em; } |
||||||
|
.svg-inline--fa.fa-w-17 { |
||||||
|
width: 1.0625em; } |
||||||
|
.svg-inline--fa.fa-w-18 { |
||||||
|
width: 1.125em; } |
||||||
|
.svg-inline--fa.fa-w-19 { |
||||||
|
width: 1.1875em; } |
||||||
|
.svg-inline--fa.fa-w-20 { |
||||||
|
width: 1.25em; } |
||||||
|
.svg-inline--fa.fa-pull-left { |
||||||
|
margin-right: .3em; |
||||||
|
width: auto; } |
||||||
|
.svg-inline--fa.fa-pull-right { |
||||||
|
margin-left: .3em; |
||||||
|
width: auto; } |
||||||
|
.svg-inline--fa.fa-border { |
||||||
|
height: 1.5em; } |
||||||
|
.svg-inline--fa.fa-li { |
||||||
|
width: 2em; } |
||||||
|
.svg-inline--fa.fa-fw { |
||||||
|
width: 1.25em; } |
||||||
|
|
||||||
|
.fa-layers svg.svg-inline--fa { |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
margin: auto; |
||||||
|
position: absolute; |
||||||
|
right: 0; |
||||||
|
top: 0; } |
||||||
|
|
||||||
|
.fa-layers { |
||||||
|
display: inline-block; |
||||||
|
height: 1em; |
||||||
|
position: relative; |
||||||
|
text-align: center; |
||||||
|
vertical-align: -.125em; |
||||||
|
width: 1em; } |
||||||
|
.fa-layers svg.svg-inline--fa { |
||||||
|
-webkit-transform-origin: center center; |
||||||
|
transform-origin: center center; } |
||||||
|
|
||||||
|
.fa-layers-text, .fa-layers-counter { |
||||||
|
display: inline-block; |
||||||
|
position: absolute; |
||||||
|
text-align: center; } |
||||||
|
|
||||||
|
.fa-layers-text { |
||||||
|
left: 50%; |
||||||
|
top: 50%; |
||||||
|
-webkit-transform: translate(-50%, -50%); |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
-webkit-transform-origin: center center; |
||||||
|
transform-origin: center center; } |
||||||
|
|
||||||
|
.fa-layers-counter { |
||||||
|
background-color: #ff253a; |
||||||
|
border-radius: 1em; |
||||||
|
-webkit-box-sizing: border-box; |
||||||
|
box-sizing: border-box; |
||||||
|
color: #fff; |
||||||
|
height: 1.5em; |
||||||
|
line-height: 1; |
||||||
|
max-width: 5em; |
||||||
|
min-width: 1.5em; |
||||||
|
overflow: hidden; |
||||||
|
padding: .25em; |
||||||
|
right: 0; |
||||||
|
text-overflow: ellipsis; |
||||||
|
top: 0; |
||||||
|
-webkit-transform: scale(0.25); |
||||||
|
transform: scale(0.25); |
||||||
|
-webkit-transform-origin: top right; |
||||||
|
transform-origin: top right; } |
||||||
|
|
||||||
|
.fa-layers-bottom-right { |
||||||
|
bottom: 0; |
||||||
|
right: 0; |
||||||
|
top: auto; |
||||||
|
-webkit-transform: scale(0.25); |
||||||
|
transform: scale(0.25); |
||||||
|
-webkit-transform-origin: bottom right; |
||||||
|
transform-origin: bottom right; } |
||||||
|
|
||||||
|
.fa-layers-bottom-left { |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
right: auto; |
||||||
|
top: auto; |
||||||
|
-webkit-transform: scale(0.25); |
||||||
|
transform: scale(0.25); |
||||||
|
-webkit-transform-origin: bottom left; |
||||||
|
transform-origin: bottom left; } |
||||||
|
|
||||||
|
.fa-layers-top-right { |
||||||
|
right: 0; |
||||||
|
top: 0; |
||||||
|
-webkit-transform: scale(0.25); |
||||||
|
transform: scale(0.25); |
||||||
|
-webkit-transform-origin: top right; |
||||||
|
transform-origin: top right; } |
||||||
|
|
||||||
|
.fa-layers-top-left { |
||||||
|
left: 0; |
||||||
|
right: auto; |
||||||
|
top: 0; |
||||||
|
-webkit-transform: scale(0.25); |
||||||
|
transform: scale(0.25); |
||||||
|
-webkit-transform-origin: top left; |
||||||
|
transform-origin: top left; } |
||||||
|
|
||||||
|
.fa-lg { |
||||||
|
font-size: 1.33333em; |
||||||
|
line-height: 0.75em; |
||||||
|
vertical-align: -.0667em; } |
||||||
|
|
||||||
|
.fa-xs { |
||||||
|
font-size: .75em; } |
||||||
|
|
||||||
|
.fa-sm { |
||||||
|
font-size: .875em; } |
||||||
|
|
||||||
|
.fa-1x { |
||||||
|
font-size: 1em; } |
||||||
|
|
||||||
|
.fa-2x { |
||||||
|
font-size: 2em; } |
||||||
|
|
||||||
|
.fa-3x { |
||||||
|
font-size: 3em; } |
||||||
|
|
||||||
|
.fa-4x { |
||||||
|
font-size: 4em; } |
||||||
|
|
||||||
|
.fa-5x { |
||||||
|
font-size: 5em; } |
||||||
|
|
||||||
|
.fa-6x { |
||||||
|
font-size: 6em; } |
||||||
|
|
||||||
|
.fa-7x { |
||||||
|
font-size: 7em; } |
||||||
|
|
||||||
|
.fa-8x { |
||||||
|
font-size: 8em; } |
||||||
|
|
||||||
|
.fa-9x { |
||||||
|
font-size: 9em; } |
||||||
|
|
||||||
|
.fa-10x { |
||||||
|
font-size: 10em; } |
||||||
|
|
||||||
|
.fa-fw { |
||||||
|
text-align: center; |
||||||
|
width: 1.25em; } |
||||||
|
|
||||||
|
.fa-ul { |
||||||
|
list-style-type: none; |
||||||
|
margin-left: 2.5em; |
||||||
|
padding-left: 0; } |
||||||
|
.fa-ul > li { |
||||||
|
position: relative; } |
||||||
|
|
||||||
|
.fa-li { |
||||||
|
left: -2em; |
||||||
|
position: absolute; |
||||||
|
text-align: center; |
||||||
|
width: 2em; |
||||||
|
line-height: inherit; } |
||||||
|
|
||||||
|
.fa-border { |
||||||
|
border: solid 0.08em #eee; |
||||||
|
border-radius: .1em; |
||||||
|
padding: .2em .25em .15em; } |
||||||
|
|
||||||
|
.fa-pull-left { |
||||||
|
float: left; } |
||||||
|
|
||||||
|
.fa-pull-right { |
||||||
|
float: right; } |
||||||
|
|
||||||
|
.fa.fa-pull-left, |
||||||
|
.fas.fa-pull-left, |
||||||
|
.far.fa-pull-left, |
||||||
|
.fal.fa-pull-left, |
||||||
|
.fab.fa-pull-left { |
||||||
|
margin-right: .3em; } |
||||||
|
|
||||||
|
.fa.fa-pull-right, |
||||||
|
.fas.fa-pull-right, |
||||||
|
.far.fa-pull-right, |
||||||
|
.fal.fa-pull-right, |
||||||
|
.fab.fa-pull-right { |
||||||
|
margin-left: .3em; } |
||||||
|
|
||||||
|
.fa-spin { |
||||||
|
-webkit-animation: fa-spin 2s infinite linear; |
||||||
|
animation: fa-spin 2s infinite linear; } |
||||||
|
|
||||||
|
.fa-pulse { |
||||||
|
-webkit-animation: fa-spin 1s infinite steps(8); |
||||||
|
animation: fa-spin 1s infinite steps(8); } |
||||||
|
|
||||||
|
@-webkit-keyframes fa-spin { |
||||||
|
0% { |
||||||
|
-webkit-transform: rotate(0deg); |
||||||
|
transform: rotate(0deg); } |
||||||
|
100% { |
||||||
|
-webkit-transform: rotate(360deg); |
||||||
|
transform: rotate(360deg); } } |
||||||
|
|
||||||
|
@keyframes fa-spin { |
||||||
|
0% { |
||||||
|
-webkit-transform: rotate(0deg); |
||||||
|
transform: rotate(0deg); } |
||||||
|
100% { |
||||||
|
-webkit-transform: rotate(360deg); |
||||||
|
transform: rotate(360deg); } } |
||||||
|
|
||||||
|
.fa-rotate-90 { |
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; |
||||||
|
-webkit-transform: rotate(90deg); |
||||||
|
transform: rotate(90deg); } |
||||||
|
|
||||||
|
.fa-rotate-180 { |
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; |
||||||
|
-webkit-transform: rotate(180deg); |
||||||
|
transform: rotate(180deg); } |
||||||
|
|
||||||
|
.fa-rotate-270 { |
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; |
||||||
|
-webkit-transform: rotate(270deg); |
||||||
|
transform: rotate(270deg); } |
||||||
|
|
||||||
|
.fa-flip-horizontal { |
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; |
||||||
|
-webkit-transform: scale(-1, 1); |
||||||
|
transform: scale(-1, 1); } |
||||||
|
|
||||||
|
.fa-flip-vertical { |
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; |
||||||
|
-webkit-transform: scale(1, -1); |
||||||
|
transform: scale(1, -1); } |
||||||
|
|
||||||
|
.fa-flip-both, .fa-flip-horizontal.fa-flip-vertical { |
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; |
||||||
|
-webkit-transform: scale(-1, -1); |
||||||
|
transform: scale(-1, -1); } |
||||||
|
|
||||||
|
:root .fa-rotate-90, |
||||||
|
:root .fa-rotate-180, |
||||||
|
:root .fa-rotate-270, |
||||||
|
:root .fa-flip-horizontal, |
||||||
|
:root .fa-flip-vertical, |
||||||
|
:root .fa-flip-both { |
||||||
|
-webkit-filter: none; |
||||||
|
filter: none; } |
||||||
|
|
||||||
|
.fa-stack { |
||||||
|
display: inline-block; |
||||||
|
height: 2em; |
||||||
|
position: relative; |
||||||
|
width: 2.5em; } |
||||||
|
|
||||||
|
.fa-stack-1x, |
||||||
|
.fa-stack-2x { |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
margin: auto; |
||||||
|
position: absolute; |
||||||
|
right: 0; |
||||||
|
top: 0; } |
||||||
|
|
||||||
|
.svg-inline--fa.fa-stack-1x { |
||||||
|
height: 1em; |
||||||
|
width: 1.25em; } |
||||||
|
|
||||||
|
.svg-inline--fa.fa-stack-2x { |
||||||
|
height: 2em; |
||||||
|
width: 2.5em; } |
||||||
|
|
||||||
|
.fa-inverse { |
||||||
|
color: #fff; } |
||||||
|
|
||||||
|
.sr-only { |
||||||
|
border: 0; |
||||||
|
clip: rect(0, 0, 0, 0); |
||||||
|
height: 1px; |
||||||
|
margin: -1px; |
||||||
|
overflow: hidden; |
||||||
|
padding: 0; |
||||||
|
position: absolute; |
||||||
|
width: 1px; } |
||||||
|
|
||||||
|
.sr-only-focusable:active, .sr-only-focusable:focus { |
||||||
|
clip: auto; |
||||||
|
height: auto; |
||||||
|
margin: 0; |
||||||
|
overflow: visible; |
||||||
|
position: static; |
||||||
|
width: auto; } |
||||||
|
|
||||||
|
.svg-inline--fa .fa-primary { |
||||||
|
fill: var(--fa-primary-color, currentColor); |
||||||
|
opacity: 1; |
||||||
|
opacity: var(--fa-primary-opacity, 1); } |
||||||
|
|
||||||
|
.svg-inline--fa .fa-secondary { |
||||||
|
fill: var(--fa-secondary-color, currentColor); |
||||||
|
opacity: 0.4; |
||||||
|
opacity: var(--fa-secondary-opacity, 0.4); } |
||||||
|
|
||||||
|
.svg-inline--fa.fa-swap-opacity .fa-primary { |
||||||
|
opacity: 0.4; |
||||||
|
opacity: var(--fa-secondary-opacity, 0.4); } |
||||||
|
|
||||||
|
.svg-inline--fa.fa-swap-opacity .fa-secondary { |
||||||
|
opacity: 1; |
||||||
|
opacity: var(--fa-primary-opacity, 1); } |
||||||
|
|
||||||
|
.svg-inline--fa mask .fa-primary, |
||||||
|
.svg-inline--fa mask .fa-secondary { |
||||||
|
fill: black; } |
||||||
|
|
||||||
|
.fad.fa-inverse { |
||||||
|
color: #fff; } |
After Width: | Height: | Size: 24 KiB |
@ -0,0 +1,8 @@ |
|||||||
|
function stashrToast(message, type) { |
||||||
|
Vue.$toast.open({ |
||||||
|
message: message, |
||||||
|
type: type, |
||||||
|
position: 'bottom-left', |
||||||
|
dismissable: true |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
{ |
||||||
|
"short_name": "Stashr", |
||||||
|
"name": "Stashr Comics", |
||||||
|
"icons": [ |
||||||
|
{ |
||||||
|
"src": "/static/assets/stashr-192.png", |
||||||
|
"type": "image/png", |
||||||
|
"sizes": "192x192" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"src": "/static/assets/stashr-512.png", |
||||||
|
"type": "image/png", |
||||||
|
"sizes": "512x512" |
||||||
|
} |
||||||
|
], |
||||||
|
"start_url": "/", |
||||||
|
"background_color": "#3367D6", |
||||||
|
"display": "fullscreen", |
||||||
|
"scope": "/", |
||||||
|
"theme_color": "#3367D6" |
||||||
|
} |
After Width: | Height: | Size: 730 KiB |
After Width: | Height: | Size: 141 KiB |
After Width: | Height: | Size: 896 KiB |
@ -0,0 +1,115 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
""" |
||||||
|
Stashr - Task Calls |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2020 Andrew Vanderbye |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
||||||
|
""" |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- IMPORTS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- HUEY IMPORT --- """ |
||||||
|
from stashr.stashr import huey |
||||||
|
from huey import crontab |
||||||
|
|
||||||
|
""" --- PYTHON IMPORTS --- """ |
||||||
|
|
||||||
|
""" --- STASHR DEPENDENCY IMPORTS --- """ |
||||||
|
|
||||||
|
""" --- STASHR CORE IMPORTS --- """ |
||||||
|
from stashr import log, utils |
||||||
|
|
||||||
|
""" --- CREATE LOGGER --- """ |
||||||
|
logger = log.stashr_logger(__name__) |
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- TASKS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
""" --- SUBSCRIPTION TASKS --- """ |
||||||
|
|
||||||
|
""" --- SERIES TASKS --- """ |
||||||
|
|
||||||
|
""" --- RELEASE LIST TASKS --- """ |
||||||
|
|
||||||
|
|
||||||
|
# UPDATE RELEASE LIST |
||||||
|
@huey.periodic_task(crontab(minute='*/120')) |
||||||
|
def update_release_list_task(): |
||||||
|
logger.debug('Update New Release Task') |
||||||
|
utils.update_release_list() |
||||||
|
|
||||||
|
|
||||||
|
""" --- COMIC STATUS TASKS --- """ |
||||||
|
|
||||||
|
""" --- READING LIST TASKS --- """ |
||||||
|
|
||||||
|
""" --- COLLECTION TASKS --- """ |
||||||
|
|
||||||
|
""" --- SERVER TASKS --- """ |
||||||
|
|
||||||
|
""" --- PLUGIN TASKS --- """ |
||||||
|
|
||||||
|
"""" --- FILE TASKS --- """ |
||||||
|
|
||||||
|
""" --- DATABASE TASKS --- """ |
||||||
|
|
||||||
|
""" --- NEW TASKS --- """ |
||||||
|
|
||||||
|
|
||||||
|
@huey.task() |
||||||
|
def scan_directories(): |
||||||
|
utils.new_scan_directories() |
||||||
|
utils.new_create_scrape_entries() |
||||||
|
pass |
||||||
|
|
||||||
|
|
||||||
|
@huey.task() |
||||||
|
def add_scraped_directories(): |
||||||
|
utils.new_add_scraped_matches() |
||||||
|
pass |
||||||
|
|
||||||
|
|
||||||
|
""" --- HUEY CONFIGURE --- """ |
||||||
|
|
||||||
|
|
||||||
|
@huey.on_startup() |
||||||
|
def huey_startup(): |
||||||
|
log.disable_huey_logger() |
||||||
|
|
||||||
|
|
||||||
|
"""------------------------------------------------------------------------------------------- |
||||||
|
-- SIGNALS |
||||||
|
-------------------------------------------------------------------------------------------""" |
||||||
|
|
||||||
|
|
||||||
|
@huey.signal() |
||||||
|
def task_signal(signal, task, exc=None): |
||||||
|
logger.info('[{0}] {1}'.format(signal, task)) |
||||||
|
if signal == 'error': |
||||||
|
logger.error('[{0}] {1} - {2}'.format(signal, task, exc)) |
@ -0,0 +1,117 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('all_collections_page_header') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<collections v-bind:collections='collections'></collections> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block modals %} |
||||||
|
{{ emit_tep('all_collections_modals') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
{{ emit_tep('all_collections_button_container') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('collections', { |
||||||
|
props: ['collections'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="mb-3 px-5"> |
||||||
|
<input type="text" v-model="search" class="form-control" placeholder="Search Collections..." /> |
||||||
|
</div> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center"> |
||||||
|
<collection |
||||||
|
v-for="collection in filteredList" |
||||||
|
v-bind:collection="collection" |
||||||
|
v-bind:key="collection.collection_id" |
||||||
|
></collection> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
data() { return { search: '', } }, |
||||||
|
computed: { |
||||||
|
filteredList() { |
||||||
|
return this.collections.filter(collection => { |
||||||
|
return collection.collection_name.toLowerCase().includes(this.search.toLowerCase()) |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('collection', { |
||||||
|
props: ['collection'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded'> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> |
||||||
|
<a href="#">[[ collection.collection_name ]]</a> |
||||||
|
</div> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> |
||||||
|
<a class="stashr-poster_link" v-bind:href="'/collections/'+collection.collection_slug"> |
||||||
|
<img class="w-100" loading="lazy" v-bind:src="'/images/issues/'+collection.collection_cover_image+'.jpg'" onerror="this.src='/static/assets/cover.svg'" /> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
data() { return { hover: false, } }, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
collections: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
/* |
||||||
|
axios.get('{{ url_for('api.api_get_all_collections') }}') |
||||||
|
.then(res => this.collections = res.data.results) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
*/ |
||||||
|
this.getCollections() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getCollections() { |
||||||
|
axios.get('{{ url_for('api.api_get_all_collections') }}', { |
||||||
|
params: { |
||||||
|
offset: this.collections.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.collections.push(result) |
||||||
|
}) |
||||||
|
if(this.collections.length < res.data.number_of_total_results) { |
||||||
|
this.getCollections() |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{{ emit_tep('all_collections_page_script') }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,137 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('all_publishers_page_header') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div class="hidden"> |
||||||
|
<script type="text/javascript"> |
||||||
|
<!--//--><![CDATA[//><!-- |
||||||
|
var images = new Array() |
||||||
|
function preload() { |
||||||
|
for (i = 0; i < preload.arguments.length; i++) { |
||||||
|
images[i] = new Image() |
||||||
|
images[i].src = preload.arguments[i] |
||||||
|
} |
||||||
|
} |
||||||
|
preload( |
||||||
|
"{{ url_for('static', filename='/assets/cover.svg') }}" |
||||||
|
) |
||||||
|
//--><!]]> |
||||||
|
</script> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<publishers v-bind:publishers='publishers'></publishers> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block modals %} |
||||||
|
{{ emit_tep('all_publishers_modals') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
{{ emit_tep('all_publishers_button_container') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('publishers', { |
||||||
|
props: ['publishers'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="mb-3 px-5"> |
||||||
|
<input type="text" v-model="search" class="form-control" placeholder="Search Publishers..." /> |
||||||
|
</div> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center"> |
||||||
|
<publisher |
||||||
|
v-for="publisher in filteredList" |
||||||
|
v-bind:publisher="publisher" |
||||||
|
v-bind:key="publisher.publisher_id" |
||||||
|
></publisher> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
search: '', |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
filteredList() { |
||||||
|
return this.publishers.filter(publisher => { |
||||||
|
return publisher.publisher_name.toLowerCase().includes(this.search.toLowerCase()) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('publisher',{ |
||||||
|
props: ['publisher'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded'> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> |
||||||
|
<a href="#">[[ publisher.publisher_name ]]</a> |
||||||
|
</div> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> |
||||||
|
<a class="stashr-poster_link align-middle" :href="'{{ url_for('single_publisher_page', publisher_id='PUBLISHERID') }}'.replace('PUBLISHERID', publisher.publisher_id)"> |
||||||
|
<img class="w-100" loading="lazy" v-bind:src=publisher.publisher_image /> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
hover: false, |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
publishers: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getPublishers() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getPublishers() { |
||||||
|
axios.get('{{ url_for('api.api_get_all_publishers') }}', { |
||||||
|
params: { |
||||||
|
offset: this.publishers.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.publishers.push(result) |
||||||
|
}) |
||||||
|
if(this.publishers.length < res.data.number_of_total_results) { |
||||||
|
this.getPublishers() |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}); |
||||||
|
|
||||||
|
{{ emit_tep('all_publishers_page_script') }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,156 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('all_volumes_page_header') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div class="hidden"> |
||||||
|
<script type="text/javascript"> |
||||||
|
<!--//--><![CDATA[//><!-- |
||||||
|
var images = new Array() |
||||||
|
function preload() { |
||||||
|
for (i = 0; i < preload.arguments.length; i++) { |
||||||
|
images[i] = new Image() |
||||||
|
images[i].src = preload.arguments[i] |
||||||
|
} |
||||||
|
} |
||||||
|
preload( |
||||||
|
"{{ url_for('static', filename='/assets/cover.svg') }}" |
||||||
|
) |
||||||
|
//--><!]]> |
||||||
|
</script> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<volumes v-bind:volumes='volumesList'></volumes> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block modals %} |
||||||
|
{{ emit_tep('all_volumes_modals') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
{% if current_user.role == 'admin' %} |
||||||
|
<button type="button" class="btn btn-outline-info btn-circle btn-md" data-bs-toggle="tooltip" data-bs-placement="top" title="Add Volume from Comicvine" onclick="location.href='{{ url_for('search_page') }}';"> |
||||||
|
<i class="text-white fas fa-2x fa-plus"></i> |
||||||
|
</button> |
||||||
|
{% endif %} |
||||||
|
{{ emit_tep('all_volumes_button_container') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('volume-item', { |
||||||
|
props: ['volume'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded'> |
||||||
|
<div class="stashr-badge_tl badge rounded-pill bg-info border">[[ volume.age_rating[0].rating_short ]]</div> |
||||||
|
<div class="stashr-badge_tr badge rounded-pill bg-primary border">[[ volume.volume_have ]]/[[ volume.volume_total ]]</div> |
||||||
|
<div class="stashr-badge_br badge rounded-pill border" :class="statusClass">[[ statusWord ]]</div> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> |
||||||
|
<a :href="'/volumes/'+volume.volume_slug">[[ volume.volume_name ]]</a> |
||||||
|
</div> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> |
||||||
|
<a class="stashr-poster_link" :href="'/volumes/'+volume.volume_slug"> |
||||||
|
<img class="w-100" loading="lazy" v-bind:src="'/images/volumes/'+volume.volume_id+'.jpg'" @error="$event.target.src=volume.volume_image_med"/> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
computed: { |
||||||
|
statusClass() { |
||||||
|
let classname = 'bg-danger'; |
||||||
|
if(this.volume.volume_status) { |
||||||
|
classname = 'bg-success'; |
||||||
|
}; |
||||||
|
return classname; |
||||||
|
}, |
||||||
|
statusWord() { |
||||||
|
let status = 'ENDED'; |
||||||
|
if(this.volume.volume_status) { |
||||||
|
status = 'ONGOING'; |
||||||
|
}; |
||||||
|
return status; |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { return { hover: false } }, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('volumes', { |
||||||
|
props: ['volumes'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="mb-3 px-5"> |
||||||
|
<input type="text" v-model="search" class="form-control" placeholder="Search Volumes..." /> |
||||||
|
</div> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center"> |
||||||
|
<volume-item |
||||||
|
v-for="volume in filteredList" |
||||||
|
v-bind:volume="volume" |
||||||
|
v-bind:key="volume.volume_id" |
||||||
|
></volume-item> |
||||||
|
</ul> |
||||||
|
<!-- |
||||||
|
<i class="text-primary fas fa-spinner fa-spin fa-3x" v-if="loading"></i> |
||||||
|
--> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
data() { return { loading: true, search: '', } }, |
||||||
|
computed: { |
||||||
|
filteredList() { |
||||||
|
return this.volumes.filter(volume => { |
||||||
|
return volume.volume_name.toLowerCase().includes(this.search.toLowerCase()) |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
volumesList: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getVolumes() |
||||||
|
}, |
||||||
|
methods:{ |
||||||
|
getVolumes() { |
||||||
|
axios.get('{{ url_for('api.api_get_all_volumes') }}', { |
||||||
|
params: { |
||||||
|
offset: this.volumesList.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.volumesList.push(result) |
||||||
|
}) |
||||||
|
if(this.volumesList.length < res.data.number_of_total_results) { |
||||||
|
this.getVolumes() |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}); |
||||||
|
|
||||||
|
{{ emit_tep('all_volumes_page_script') }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,203 @@ |
|||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no"> |
||||||
|
|
||||||
|
<title>Stashr - {{ title }}</title> |
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/bootstrap.css') }}"> |
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}"> |
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/vue-toast-notification/dist/theme-sugar.css" rel="stylesheet"> |
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/all.css') }}"> |
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Grand+Hotel" rel="stylesheet"> |
||||||
|
<link href="https://fonts.googleapis.com/css?family=Oswald" rel="stylesheet"> |
||||||
|
|
||||||
|
<link rel="manifest" href="{{ url_for('static', filename='manifest/manifest.json') }}"> |
||||||
|
|
||||||
|
<link rel="apple-touch-icon" href="touch-icon-iphone.png"> |
||||||
|
<link rel="apple-touch-icon" sizes="152x152" href="{{ url_for('static', filename='assets/stashr-152.png') }}"> |
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='assets/stashr-180.png') }}"> |
||||||
|
<link rel="apple-touch-icon" sizes="167x167" href="{{ url_for('static', filename='assets/stashr-167.png') }}"> |
||||||
|
<meta name="apple-mobile-web-app-title" content="Stashr"> |
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes"> |
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default"> |
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/stashr.js') }}"></script> |
||||||
|
|
||||||
|
<!-- VueJS --> |
||||||
|
<script src="{{ url_for('static', filename='js/vue.dev.js') }}"></script> |
||||||
|
<!-- VueJS Toasts --> |
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue-toast-notification"></script> |
||||||
|
|
||||||
|
<!-- START HEADER SCRIPT INCLUDES --> |
||||||
|
{% block header_script_files %}{% endblock %} |
||||||
|
{{ emit_tep('base_page_header_script_files') }} |
||||||
|
<!-- END HEADER SCRIPT INCLUDES --> |
||||||
|
|
||||||
|
</head> |
||||||
|
<body> |
||||||
|
|
||||||
|
<!-- START NAVBAR --> |
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary sticky-top"> |
||||||
|
<div class="container-fluid"> |
||||||
|
<a class="navbar-brand" href="{{ url_for('index_page') }}"> |
||||||
|
<img class="border rounded-circle" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="40" height="40" /> |
||||||
|
<span class="stashr-project_title">Stashr</span> |
||||||
|
</a> |
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarStashr" aria-cointrols="navbarStashr" aria-expanded="false" aria-label="Toggle Navigation"> |
||||||
|
<span class="navbar-toggler-icon"></span> |
||||||
|
</button> |
||||||
|
|
||||||
|
{% if not current_user.is_authenticated %} |
||||||
|
{% if not request.endpoint == 'first_run_page' %} |
||||||
|
<div class="collapse navbar-collapse" id="navbarStashr"> |
||||||
|
<ul class="navbar-nav ms-auto"> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link" href="{{ url_for('login_page') }}"> |
||||||
|
LOGIN |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{% if open_registration %} |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link" href="{{ url_for('register_page') }}"> |
||||||
|
REGISTER |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{% endif %} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
{% if current_user.is_authenticated %} |
||||||
|
<div class="collapse navbar-collapse" id="navbarStashr"> |
||||||
|
<ul class="navbar-nav me-auto"> |
||||||
|
<li class="nav-item dropdown"> |
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="libraryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> |
||||||
|
LIBRARY |
||||||
|
</a> |
||||||
|
<ul class="dropdown-menu" aria-labelledby="libraryDropdown"> |
||||||
|
<li><a class="dropdown-item" href="{{ url_for('all_volumes_page') }}">VOLUMES</a></li> |
||||||
|
<li><a class="dropdown-item" href="{{ url_for('all_publishers_page') }}">PUBLISHERS</a></li> |
||||||
|
{% if current_user.role == 'admin' %} |
||||||
|
<li><a class="dropdown-item" href="{{ url_for('scrape_folders_page') }}">SCRAPE</a></li> |
||||||
|
{% endif %} |
||||||
|
</ul> |
||||||
|
</li> |
||||||
|
{% if current_user.role == 'admin' or current_user.role == 'librarian' %} |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('new_releases_page') %} active{% endif %}" href="{{ url_for('new_releases_page') }}"> |
||||||
|
NEW RELEASES |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{% endif %} |
||||||
|
{% if current_user.role == 'admin' or current_user.role == 'librarian' or current_user.role == 'reader' %} |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('reading_list_page') %} active{% endif %}" href="{{ url_for('reading_list_page') }}"> |
||||||
|
READING LIST |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('all_collections_page') %} active{% endif %}" href="{{ url_for('all_collections_page') }}"> |
||||||
|
COLLECTIONS |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{% endif %} |
||||||
|
{{ emit_tep("base_page_main_menu") }} |
||||||
|
</ul> |
||||||
|
<ul class="navbar-nav ms-auto"> |
||||||
|
{% if current_user.role == 'admin' %} |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link" href="{{ url_for('settings_page') }}"> |
||||||
|
<i class="fa fa-cogs"></i> |
||||||
|
Settings |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{% endif %} |
||||||
|
<li class="nav-item dropdown"> |
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="libraryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> |
||||||
|
<i class="fa fa-user"></i> |
||||||
|
{{ current_user.username }} |
||||||
|
</a> |
||||||
|
<ul class="dropdown-menu" aria-labelledby="libraryDropdown"> |
||||||
|
<li><a class="dropdown-item" href="{{ url_for('settings_single_user_page', user_id=current_user.id) }}"> |
||||||
|
<i class="fa fa-user"></i> |
||||||
|
{{ current_user.username }} |
||||||
|
</a></li> |
||||||
|
<li><a class="dropdown-item" href="{{ url_for('logout_page') }}"> |
||||||
|
<i class="fas fa-sign-out-alt"></i> |
||||||
|
Logout |
||||||
|
</a></li> |
||||||
|
</ul> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
|
||||||
|
</div> |
||||||
|
</nav> |
||||||
|
|
||||||
|
<!-- END NAVBAR --> |
||||||
|
|
||||||
|
<!-- START HEADER --> |
||||||
|
<div class="py-2" id="stashr_header"> |
||||||
|
{% block header %}{% endblock %} |
||||||
|
</div> |
||||||
|
<!-- END HEADER --> |
||||||
|
|
||||||
|
<!-- START CONTENT --> |
||||||
|
<div class="py-2" id="stashr_content"> |
||||||
|
{% block content %}{% endblock %} |
||||||
|
</div> |
||||||
|
<!-- END CONTENT --> |
||||||
|
|
||||||
|
<!-- START MODALS --> |
||||||
|
{% block modals %}{% endblock %} |
||||||
|
{{ emit_tep('base_page_modals') }} |
||||||
|
<!-- END MODALS --> |
||||||
|
|
||||||
|
<!-- START BUTTON CONTAINER --> |
||||||
|
<div class="stashr-button_container p-2"> |
||||||
|
{% block button_container %}{% endblock %} |
||||||
|
</div> |
||||||
|
<!-- END BUTTON CONTAINER --> |
||||||
|
|
||||||
|
<!-- START FOOTER SCRIPT INCLUDES --> |
||||||
|
<script src="{{ url_for('static', filename='js/bootstrap.bundle.js') }}"></script> |
||||||
|
{% block footer_script_files %}{% endblock %} |
||||||
|
{{ emit_tep('base_page_footer_script_files') }} |
||||||
|
<!-- END FOOTER SCRIPT INCLUDES --> |
||||||
|
|
||||||
|
<!-- START FOOTER SCRIPT --> |
||||||
|
<script type="text/javascript"> |
||||||
|
|
||||||
|
/* ----- CODE FOR TOOLTIPS WHEN WORKING |
||||||
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) |
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { |
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl) |
||||||
|
}) |
||||||
|
*/ |
||||||
|
|
||||||
|
Vue.use(VueToast); |
||||||
|
|
||||||
|
// Flashes |
||||||
|
{% with flashes = get_flashed_messages(with_categories=true) %} |
||||||
|
{% if flashes %} |
||||||
|
{% for category, message in flashes %} |
||||||
|
stashrToast('{{ message }}', '{{ category }}'); |
||||||
|
{% endfor %} |
||||||
|
{% endif %} |
||||||
|
{% endwith %} |
||||||
|
|
||||||
|
{% block script %}{% endblock %} |
||||||
|
{{ emit_tep('base_page_script') }} |
||||||
|
|
||||||
|
</script> |
||||||
|
<!-- END FOOTER SCRIPT --> |
||||||
|
|
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,52 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
<div class="d-flex justify-content-center flex-wrap w-80"> |
||||||
|
<div class='position-relative my-2'> |
||||||
|
<img class="border rounded-circle my-3" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="200" height="200" /> |
||||||
|
</div> |
||||||
|
<div class="bg-light m-2 px-2 rounded stashr-series_info text-center text-lg-start py-3 px-5"> |
||||||
|
<h5 class="text-center">First Run</h5> |
||||||
|
<hr /> |
||||||
|
<form method="POST"> |
||||||
|
|
||||||
|
{{ first_run_form.csrf_token }} |
||||||
|
|
||||||
|
<div class="mb-3"> |
||||||
|
{{ first_run_form.username.label }} |
||||||
|
{{ first_run_form.username(class_='form-control', placeholder=first_run_form.username.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ first_run_form.email.label }} |
||||||
|
{{ first_run_form.email(class_='form-control', placeholder=first_run_form.email.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ first_run_form.password.label }} |
||||||
|
{{ first_run_form.password(type='password', class_='form-control', placeholder=first_run_form.password.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ first_run_form.confirm_password.label }} |
||||||
|
{{ first_run_form.confirm_password(type='password', class_='form-control', placeholder=first_run_form.confirm_password.label.text) }} |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ first_run_form.comicvine_api_key.label }} |
||||||
|
{{ first_run_form.comicvine_api_key(class_='form-control', placeholder=first_run_form.comicvine_api_key.label.text) }} |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ first_run_form.open_registration }} |
||||||
|
{{ first_run_form.open_registration.label }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ first_run_form.logging_level.label }} |
||||||
|
{{ first_run_form.logging_level(class_='form-control') }} |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="mb-3 text-end"> |
||||||
|
{{ first_run_form.first_run_button(class_='btn btn-success') }} |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock %} |
@ -0,0 +1,37 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center flex-wrap w-80"> |
||||||
|
<div class='position-relative my-2'> |
||||||
|
<img class="border rounded-circle my-3" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="200" height="200" /> |
||||||
|
</div> |
||||||
|
<div class="bg-light m-2 px-2 rounded stashr-series_info text-center text-lg-start py-3 px-5"> |
||||||
|
<h5 class="text-center">Forgot Password</h5> |
||||||
|
<hr /> |
||||||
|
<form action="{{ url_for('forgot_page') }}" method="post" id="forgot_password_form"> |
||||||
|
{% if forgot_password_form.errors %} |
||||||
|
<div class="notification bg-danger rounded m-3 text-white" role="alert" id='error_container'> |
||||||
|
{% for field_name, field_errors in forgot_password_form.errors|dictsort if field_errors %} |
||||||
|
<ul> |
||||||
|
{% for error in field_errors %} |
||||||
|
<li>{{ error }}</li> |
||||||
|
{% endfor %} |
||||||
|
</ul> |
||||||
|
{% endfor %} |
||||||
|
</div> |
||||||
|
{% else %} |
||||||
|
{% endif %} |
||||||
|
{{ forgot_password_form.csrf_token }} |
||||||
|
<div class="mb-3"> |
||||||
|
{{ forgot_password_form.email(class_='input form-control', placeholder='E-Mail') }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ forgot_password_form.forgot_button(class_='btn btn-outline-success') }} |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,26 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div class="d-flex justify-content-center flex-wrap w-80"> |
||||||
|
<div class='position-relative my-2'> |
||||||
|
<img class="border rounded-circle my-3" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="200" height="200" /> |
||||||
|
</div> |
||||||
|
<div class="bg-light m-2 px-2 rounded stashr-series_info text-center text-lg-start py-3 px-5"> |
||||||
|
<h1 class="stashr-project_title">Stashr</h1> |
||||||
|
<h5>Your Comic Book Collection</h5> |
||||||
|
<h6>Manage and Read your Digital Collection</h6> |
||||||
|
{% if not current_user.is_authenticated %} |
||||||
|
<div class="row"> |
||||||
|
<div class="col-12"> |
||||||
|
<a href="{{ url_for('login_page') }}" class="btn btn-outline-primary">LOGIN</a> |
||||||
|
{% if open_registration %} |
||||||
|
<a href="{{ url_for('register_page') }}" class="btn btn-outline-primary">REGISTER</a> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,43 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div class="d-flex justify-content-center flex-wrap w-80"> |
||||||
|
<div class='position-relative my-2'> |
||||||
|
<img class="border rounded-circle my-3" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="200" height="200" /> |
||||||
|
</div> |
||||||
|
<div class="bg-light m-2 px-2 rounded stashr-series_info text-center text-lg-start py-3 px-5"> |
||||||
|
<h5 class="text-center">Login</h5> |
||||||
|
<hr /> |
||||||
|
<form action="{{ url_for('login_page') }}" method="post" id="registration_form"> |
||||||
|
{% if login_form.errors %} |
||||||
|
<div class="notification bg-danger rounded m-3 text-white" role="alert" id='error_container'> |
||||||
|
{% for field_name, field_errors in login_form.errors|dictsort if field_errors %} |
||||||
|
<ul> |
||||||
|
{% for error in field_errors %} |
||||||
|
<li>{{ error }}</li> |
||||||
|
{% endfor %} |
||||||
|
</ul> |
||||||
|
{% endfor %} |
||||||
|
</div> |
||||||
|
{% else %} |
||||||
|
{% endif %} |
||||||
|
{{ login_form.csrf_token }} |
||||||
|
<div class="mb-3"> |
||||||
|
{{ login_form.username(class_='input form-control', placeholder='Username') }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ login_form.password(class_='input form-control', type='password', placeholder='Password') }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ login_form.remember_me() }} Remember Me |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ login_form.login_button(class_='btn btn-outline-success') }} |
||||||
|
<a href="{{ url_for('forgot_page') }}" class="btn btn-outline-danger">Forgot Password</a> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,229 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('new_releases_page_header') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<releases ref='releases' v-bind:releases='releaseList'></releases> |
||||||
|
<modals v-bind:volume='volume'></modals> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
<button type="button" class="btn btn-outline-info btn-circle btn-md" data-bs-toggle="tooltip" data-bs-placement="top" title="Update New Releases" onclick="app.updateNewReleases()"> |
||||||
|
<i class="text-white fas fa-2x fa-sync-alt"></i> |
||||||
|
</button> |
||||||
|
{{ emit_tep('new_releases_page_button_container') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('modals', { |
||||||
|
props: [ |
||||||
|
'volume' |
||||||
|
], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="modalSubscription" tabindex="-1" role="dialog" aria-labelledby="subscriptionModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
[[ volume.new_release_comic_name ]] #[[ volume.new_release_issue_number ]] |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<a id="comic-have-link" class="btn btn-outline-primary" :href="volume.new_release_item_url" role="button" target="new">ComicVine Link</a> |
||||||
|
<span id="subscribeSubscribed" v-if="volume.status"> |
||||||
|
<!-- SUBSCRIBED --> |
||||||
|
<a id="comic-page" class="btn btn-outline-primary" :href="'{{ url_for('all_volumes_page') }}/' + volume.new_release_volume_id" role="button">Comic Page</a> |
||||||
|
</span> |
||||||
|
<span id="subscribeNotSubscribed" v-else> |
||||||
|
<!-- NOT SUBSCRIBED --> |
||||||
|
<button id="subscribeToVolume" class="btn btn-outline-primary" role="button" data-bs-dismiss="modal" @click='add_to_library'>Add to Library</button> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{ emit_tep('new_releases_page_modals') }} |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: { |
||||||
|
add_to_library() { |
||||||
|
console.log('adding to library') |
||||||
|
app.$refs.releases.$children.find(child => { return child.$vnode.key == this.volume.new_release_id }).subscribe_to_volume() |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('release-item', { |
||||||
|
props: ['release'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded'> |
||||||
|
|
||||||
|
<div class="stashr-badge_tl badge rounded-pill bg-info border">#[[ release.new_release_issue_number ]]</div> |
||||||
|
<div class="stashr-badge_br badge rounded-pill border px-2" :class="className" >[[ subText ]]</div> |
||||||
|
|
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> |
||||||
|
<a data-bs-toggle="modal" data-bs-target="#modalSubscription" v-on:click="this.changeModal">[[ release.new_release_comic_name ]]</a> |
||||||
|
</div> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> |
||||||
|
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalSubscription" v-on:click="this.changeModal"> |
||||||
|
<img class="w-100" loading="lazy" :src="release.new_release_image_url" /> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
computed: { |
||||||
|
className() { |
||||||
|
let classname = 'bg-danger'; |
||||||
|
try { |
||||||
|
if (this.release.status) { |
||||||
|
classname = 'bg-success'; |
||||||
|
} |
||||||
|
} finally { |
||||||
|
return classname; |
||||||
|
} |
||||||
|
}, |
||||||
|
subText() { |
||||||
|
let text = 'Not In Library'; |
||||||
|
try { |
||||||
|
if (this.release.status) { |
||||||
|
text = 'In Library'; |
||||||
|
} |
||||||
|
} finally { |
||||||
|
return text; |
||||||
|
} |
||||||
|
}, |
||||||
|
subStatus() { |
||||||
|
let status = false; |
||||||
|
try { |
||||||
|
if (this.release.status) { |
||||||
|
status = true; |
||||||
|
} |
||||||
|
} finally { |
||||||
|
return status |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
subscribe_to_volume() { |
||||||
|
axios.post('{{ url_for('api.api_post_single_volume', volume_id='VOLUMESTRING') }}'.replace('VOLUMESTRING', this.release.new_release_volume_id)) |
||||||
|
.then(res => { |
||||||
|
console.log(res) |
||||||
|
if (res.data.status_code == 200) { |
||||||
|
this.release.status = {'volume_status': true}; |
||||||
|
stashrToast('Added to Library', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
changeModal() { |
||||||
|
app.changeModal(this.release) |
||||||
|
}, |
||||||
|
}, |
||||||
|
data () { return { hover: false, subscribed: false, } }, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('releases', { |
||||||
|
props: ['releases'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="mb-3 px-5"> |
||||||
|
<input type="text" v-model="search" class="form-control" placeholder="Search New Releases..." /> |
||||||
|
</div> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center"> |
||||||
|
<release-item |
||||||
|
v-for="release in filteredList" |
||||||
|
v-bind:release="release" |
||||||
|
v-bind:key="release.new_release_id" |
||||||
|
></release-item> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
data() { return { search: '', } }, |
||||||
|
computed: { |
||||||
|
filteredList() { |
||||||
|
return this.releases.filter(release => { |
||||||
|
return release.new_release_comic_name.toLowerCase().includes(this.search.toLowerCase()) |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
releaseList: [], |
||||||
|
volume: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getNewReleases() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getNewReleases(){ |
||||||
|
axios.get('{{ url_for('api.api_get_new_releases') }}', { |
||||||
|
params: { |
||||||
|
offset: this.releaseList.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.releaseList.push(result) |
||||||
|
}); |
||||||
|
if(this.releaseList.length < res.data.number_of_total_results) { |
||||||
|
this.getNewReleases() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
updateNewReleases() { |
||||||
|
stashrToast('Updating New Releases', 'info') |
||||||
|
axios.post('{{ url_for('api.api_post_new_releases') }}') |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
this.releaseList = []; |
||||||
|
this.getNewReleases(); |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
changeModal(volume) { |
||||||
|
console.log('changing modal') |
||||||
|
this.volume = volume |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{{ emit_tep('new_releases_page_script') }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,188 @@ |
|||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>Stashr - Reading - {{ comic_name }}</title> |
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/bootstrap.css') }}"> |
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}"> |
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/all.css') }}"> |
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
<script src="https://unpkg.com/swiper/swiper-bundle.js"></script> |
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.css"> |
||||||
|
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" /> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0, minimal-ui" /> |
||||||
|
|
||||||
|
</head> |
||||||
|
|
||||||
|
<body class="stashrRead"> |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<swiper v-bind:slides='slides' v-swiper='$options.swiperOptions'></swiper> |
||||||
|
<modal></modal> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="stashr-button_container_reader p-2"> |
||||||
|
<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#modalRead"> |
||||||
|
<i class="text-white fas fa-bars fa-2x"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<script> |
||||||
|
|
||||||
|
Vue.directive('swiper', { |
||||||
|
inserted (el, binding, vnode) { |
||||||
|
console.log(binding.value) |
||||||
|
const myswiper = new Swiper(el, binding.value) |
||||||
|
}, |
||||||
|
componentUpdates() { |
||||||
|
console.log('updated?') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modal', { |
||||||
|
props: [], |
||||||
|
template: ` |
||||||
|
<div class="modal" id="modalRead" tabindex="-1" role="dialog" aria-labelledby="modalRead" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="exampleModalLongTitle">Comic Settings</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
Goto Page <input type="text" id="gotoPage" size="1" name="gotoPage" :value="this.$parent.active_page" />/[[ this.$parent.total_pages ]] <button type="button" class="btn btn-secondary" data-dismiss="modal" v-on:click="goto_slide">GO</button> |
||||||
|
{{ emit_tep("read_issue_page_modal_extension", issue_id=issue_id) }} |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal" v-on:click='mark_read'>Mark Read</button> |
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal" v-on:click='go_back'>Go Back</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: { |
||||||
|
mark_read() { |
||||||
|
console.log('mark read') |
||||||
|
app.mark_read() |
||||||
|
}, |
||||||
|
go_back() { |
||||||
|
window.history.back(); |
||||||
|
}, |
||||||
|
goto_slide() { |
||||||
|
app.goto_slide(document.getElementById("gotoPage").value) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('slide', { |
||||||
|
props: ['slide'], |
||||||
|
template: ` |
||||||
|
<div class="swiper-slide"> |
||||||
|
<div class="swiper-zoom-container"> |
||||||
|
<img :data-src="'{{ url_for('api.api_get_single_issue', issue_id=issue_id) }}/' + slide.path" class="swiper-lazy"> |
||||||
|
<div class="swiper-lazy-preloader"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
mounted() { |
||||||
|
var mySwiper = document.querySelector('.swiper-container').swiper |
||||||
|
mySwiper.update(); |
||||||
|
mySwiper.lazy.load(0); |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('swiper', { |
||||||
|
props: ['slides'], |
||||||
|
template: ` |
||||||
|
<div class="swiper-container"> |
||||||
|
<div class="swiper-wrapper"> |
||||||
|
<slide |
||||||
|
v-for="(slide, index) in slides" |
||||||
|
v-bind:slide="slide" |
||||||
|
v-bind:key="slide.id" |
||||||
|
></slide> |
||||||
|
</div> |
||||||
|
<div class="swiper-pagination"></div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
slides: [], |
||||||
|
active_page: 1, |
||||||
|
total_pages: 0 |
||||||
|
}, |
||||||
|
created() { |
||||||
|
axios.get('{{ url_for('api.api_get_single_issue', issue_id=issue_id) }}') |
||||||
|
.then(res => { |
||||||
|
this.slides = res.data.results; |
||||||
|
this.total_pages = res.data.results.length; |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
swiperOptions: { |
||||||
|
slidesPerView: 1, |
||||||
|
keyboard: { |
||||||
|
enabled: true, |
||||||
|
}, |
||||||
|
mousewheel: true, |
||||||
|
touches: { |
||||||
|
simulateTouch: false, |
||||||
|
}, |
||||||
|
lazy: { |
||||||
|
lazy: true, |
||||||
|
loadPrevNext: true, |
||||||
|
loadPrevNextAmount: 2, |
||||||
|
}, |
||||||
|
zoom: true, |
||||||
|
pagination: { |
||||||
|
el: '.swiper-pagination', |
||||||
|
type: 'progressbar', |
||||||
|
}, |
||||||
|
on: { |
||||||
|
slideChange: function() { |
||||||
|
app.update_slide(this.activeIndex) |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
update_slide(index) { |
||||||
|
this.active_page = index + 1 |
||||||
|
}, |
||||||
|
goto_slide(page) { |
||||||
|
var mySwiper = document.querySelector('.swiper-container').swiper |
||||||
|
mySwiper.slideTo(page-1) |
||||||
|
}, |
||||||
|
mark_read() { |
||||||
|
axios.put('{{ url_for('api.api_put_single_issue', issue_id=issue_id) }}', { |
||||||
|
data: { |
||||||
|
read_status: 1 |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res=> { |
||||||
|
window.history.back(); |
||||||
|
} |
||||||
|
) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/bootstrap.bundle.js') }}"></script> |
||||||
|
|
||||||
|
</body> |
||||||
|
|
||||||
|
</html> |
@ -0,0 +1,269 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
<script src="//cdn.jsdelivr.net/npm/sortablejs@1.8.4/Sortable.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('reading_list_page_header') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<issues v-bind:issues='issues' ref="issues"></issues> |
||||||
|
<modals v-bind:issue='issue'></modals> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block modals %} |
||||||
|
{{ emit_tep('reading_list_page_modals') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
<button type="button" class="btn btn-outline-danger btn-circle btn-md" data-bs-toggle="tooltip" data-bs-placement="top" title="Clear Reading List" onclick="app.clearList()"> |
||||||
|
<i class="text-white fas fa-2x fa-minus"></i> |
||||||
|
</button> |
||||||
|
{{ emit_tep('reading_list_page_button_container') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.directive('sortable', { |
||||||
|
inserted (el, binding, vnode) { |
||||||
|
console.log(binding.value); |
||||||
|
let options = binding.value; |
||||||
|
options.onUpdate = (e) => vnode.data.on.sorted(e); |
||||||
|
const sortable = Sortable.create(el, binding.value); |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modals', { |
||||||
|
props: ['issue'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="modalRead" tabindex="-1" role="dialog" aria-labelledby="readModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
[[ issue.volume.volume_name ]] #[[ issue.issue_number ]] |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body center"> |
||||||
|
<a :href="'/read/'+issue.issue_id" id="readRead" class="btn btn-success"> |
||||||
|
<i class="fas fa-book-open"></i> |
||||||
|
Read |
||||||
|
</a> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') or |
||||||
|
(current_user.role.lower() == 'patron') %} |
||||||
|
<a :href="'/api/downloads/'+issue.issue_id" id="readDownload" class="btn btn-success"> |
||||||
|
<i class="fas fa-download"></i> |
||||||
|
Download |
||||||
|
</a> |
||||||
|
{% endif %} |
||||||
|
<hr /> |
||||||
|
<button id="actionToggle" class="btn btn-info my-1" @click="toggleRead" data-bs-dismiss="modal"> |
||||||
|
<i class="fas fa-check"></i> |
||||||
|
Toggle Read |
||||||
|
</button> |
||||||
|
<button id="readRemove" class="btn btn-danger" @click="removeIssue" data-bs-dismiss="modal"> |
||||||
|
<i class="fas fa-minus"></i> |
||||||
|
Remove From Reading List |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{ emit_tep('reading_list_page_issue_modals') }} |
||||||
|
`, |
||||||
|
methods: { |
||||||
|
toggleRead() { |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).toggleRead() |
||||||
|
}, |
||||||
|
removeIssue() { |
||||||
|
app.removeIssue(this.issue.issue_id) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('issues', { |
||||||
|
props: ['issues'], |
||||||
|
template: ` |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center" v-sortable="$options.sortOptions" @sorted='handleSorted'> |
||||||
|
<issue |
||||||
|
v-for="issue in issues" |
||||||
|
v-bind:issue="issue" |
||||||
|
v-bind:key="issue.issue_id" |
||||||
|
></issue> |
||||||
|
</ul> |
||||||
|
`, |
||||||
|
methods: { |
||||||
|
handleSorted(event) { |
||||||
|
app.handleSorted(event) |
||||||
|
}, |
||||||
|
}, |
||||||
|
sortOptions: { |
||||||
|
draggable: '.js-sortable-block', |
||||||
|
handle: '.js-drag-handle', |
||||||
|
delay: 300, |
||||||
|
delayOnTouchOnly: true |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('issue', { |
||||||
|
props: ['issue'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2 js-sortable-block' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded'> |
||||||
|
<div class="stashr-badge_tl badge rounded-pill bg-dark border js-drag-handle"> |
||||||
|
<i class="fas fa-arrows-alt"></i> |
||||||
|
</div> |
||||||
|
<div class="stashr-badge_br badge rounded-pill bg-dark border"> |
||||||
|
<i class="fas fa-eye" v-bind:class="statusRead" v-on:click="toggleRead"></i> |
||||||
|
</div> |
||||||
|
<!-- |
||||||
|
<div class="stashr-badge_tr badge badge-pill badge-info border">[[ issue.reading_list_position ]]</div> |
||||||
|
--> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> |
||||||
|
<a href="#">[[ issue.volume.volume_name ]] #[[ issue.issue_number ]]</a> |
||||||
|
</div> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> |
||||||
|
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalRead" v-on:click="this.changeModal"> |
||||||
|
<img class="w-100" loading="lazy" v-bind:src="'/images/issues/'+issue.issue_id+'.jpg'"/> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
computed: { |
||||||
|
statusRead() { |
||||||
|
let classname = 'text-danger'; |
||||||
|
try { |
||||||
|
if (this.issue.read_status[0].read_status) { |
||||||
|
classname = 'text-success'; |
||||||
|
} |
||||||
|
} finally { |
||||||
|
return classname; |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
toggleRead() { |
||||||
|
axios.put('{{ url_for('api.api_put_single_issue', issue_id='ISSUEID') }}'.replace('ISSUEID', this.issue.issue_id), { |
||||||
|
data: { |
||||||
|
read_status: !this.issue.read_status[0].read_status |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Toggled Read Status', 'success'); |
||||||
|
this.issue.read_status[0].read_status = !this.issue.read_status[0].read_status; |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
changeModal() { |
||||||
|
app.changeModal(this.issue) |
||||||
|
}, |
||||||
|
}, |
||||||
|
data() { return { hover: false } }, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
issues: [], |
||||||
|
issue: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getIssues() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
|
||||||
|
getIssues() { |
||||||
|
axios.get('{{ url_for('api.api_get_reading_list') }}', { |
||||||
|
params: { |
||||||
|
offset: this.issues.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then( res => { |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.issues.push(result) |
||||||
|
}) |
||||||
|
if(this.issues.length < res.data.number_of_total_results) { |
||||||
|
this.getIssues() |
||||||
|
} |
||||||
|
} |
||||||
|
this.issue = this.issues[0] |
||||||
|
}) |
||||||
|
.catch() |
||||||
|
}, |
||||||
|
|
||||||
|
removeIssue(id) { |
||||||
|
axios.delete('{{ url_for('api.api_delete_reading_list_single', issue_id='ISSUEID') }}'.replace('ISSUEID', id)) |
||||||
|
.then( |
||||||
|
this.issues = this.issues.filter(issue => issue.issue_id !== id) |
||||||
|
) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
|
||||||
|
handleSorted(event) { |
||||||
|
axios.put('{{ url_for('api.api_put_reading_list') }}', { |
||||||
|
data: { |
||||||
|
old_index: event.oldIndex, |
||||||
|
new_index: event.newIndex |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Updated Order', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
|
||||||
|
clearList() { |
||||||
|
axios.delete('{{ url_for('api.api_delete_reading_list') }}') |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Cleared Reading List', 'success') |
||||||
|
this.issues = [] |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
|
||||||
|
changeModal(issue) { |
||||||
|
this.issue = issue |
||||||
|
}, |
||||||
|
|
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{{ emit_tep('reading_list_page_script') }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,45 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div class="d-flex justify-content-center flex-wrap w-80"> |
||||||
|
<div class='position-relative my-2'> |
||||||
|
<img class="border rounded-circle my-3" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="200" height="200" /> |
||||||
|
</div> |
||||||
|
<div class="bg-light m-2 px-2 rounded stashr-series_info text-center text-lg-start py-3 px-5"> |
||||||
|
<h5 class="text-center">Register</h5> |
||||||
|
<hr /> |
||||||
|
<form action="{{ url_for('register_page') }}" method="post" id="registration_form"> |
||||||
|
{% if registration_form.errors %} |
||||||
|
<div class="notification bg-danger rounded m-3 text-white" role="alert" id='error_container'> |
||||||
|
{% for field_name, field_errors in registration_form.errors|dictsort if field_errors %} |
||||||
|
<ul> |
||||||
|
{% for error in field_errors %} |
||||||
|
<li>{{ error }}</li> |
||||||
|
{% endfor %} |
||||||
|
</ul> |
||||||
|
{% endfor %} |
||||||
|
</div> |
||||||
|
{% else %} |
||||||
|
{% endif %} |
||||||
|
{{ registration_form.csrf_token }} |
||||||
|
<div class="mb-3"> |
||||||
|
{{ registration_form.username(class_='input form-control', placeholder='Username') }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ registration_form.email(class_='input form-control', placeholder='E-Mail') }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ registration_form.reg_password(class_='input form-control', placeholder='Password', type='password') }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ registration_form.confirm_reg_password(class_='input form-control', placeholder='Confirm Password', type='password') }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ registration_form.register_button(class_='btn btn-outline-success') }} |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,290 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('scrape_page_header') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<modal ref="modal" v-bind:individual="individual"></modal> |
||||||
|
<directories v-bind:directories='directories'></directories> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block modals %} |
||||||
|
{{ emit_tep('scrape_page_modals') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
<button type="button" class="btn btn-outline-info btn-circle btn-md" data-bs-toggle="tooltip" data-bs-placement="top" title="Add Selected Directories" onclick="app.addDirectories()"> |
||||||
|
<i class="text-white fas fa-2x fa-plus"></i> |
||||||
|
</button> |
||||||
|
<button type="button" class="btn btn-outline-info btn-circle btn-md" data-bs-toggle="tooltip" data-bs-placement="top" title="Rescan New Directories" onclick="app.rescanDirectories()"> |
||||||
|
<i class="text-white fas fa-2x fa-sync-alt"></i> |
||||||
|
</button> |
||||||
|
{{ emit_tep('scrape_page_button_container') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('candidate', { |
||||||
|
props: ['candidate', 'individual'], |
||||||
|
template: ` |
||||||
|
<option :value="[[ candidate.id ]]"> |
||||||
|
[[ candidate.name ]] - [[ candidate.id ]] |
||||||
|
</option> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modal', { |
||||||
|
props: ['individual'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="modalScrape" data-something="" tabindex="-1" role="dialog" aria-labelledby="subscriptionModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg modal-dialog-scrollable"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
<span id="subscriptionName">Folder - [[ individual.scrape_directory ]]</span> |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-3"> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/folder.svg" /> |
||||||
|
<a class="stashr-poster_link"> |
||||||
|
<img class="w-100" :src="[[ match['image']['small_url'] ]]" loading="lazy"/> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="col-9"> |
||||||
|
<select id="selectVolume" class="form-select" aria-label="Default select example" @change='doSomething($event)' v-if='!this.individual.scrape_add'> |
||||||
|
<candidate |
||||||
|
v-for="item in json" |
||||||
|
v-bind:candidate="item" |
||||||
|
v-bind:individual="this.individual" |
||||||
|
v-bind:key="item['id']" |
||||||
|
></candidate> |
||||||
|
</select> |
||||||
|
<div class='py-2'> |
||||||
|
<h5>[[ match['name'] ]] ([[ match['start_year'] ]])</h5> |
||||||
|
<p v-html="match['description']"></p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- |
||||||
|
<select class="form-select" aria-label="Default select example"> |
||||||
|
<option selected>Open this select menu</option> |
||||||
|
<option value="1">One</option> |
||||||
|
<option value="2">Two</option> |
||||||
|
<option value="3">Three</option> |
||||||
|
</select> |
||||||
|
--> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{ emit_tep('scrape_page_directory_modals') }} |
||||||
|
</div> |
||||||
|
`, |
||||||
|
updated() { |
||||||
|
this.setTest(); |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
match() { |
||||||
|
return JSON.parse(this.individual.scrape_json).filter(item => { |
||||||
|
return item['id'] == this.individual.scrape_candidate |
||||||
|
})[0] |
||||||
|
}, |
||||||
|
json() { |
||||||
|
return JSON.parse(this.individual.scrape_json) |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
doSomething(something) { |
||||||
|
console.log(something.target.value) |
||||||
|
this.individual.scrape_candidate = something.target.value |
||||||
|
}, |
||||||
|
setTest() { |
||||||
|
console.log('in here') |
||||||
|
this.test = JSON.parse(this.individual.scrape_json).filter(item => { |
||||||
|
return item['id'] == this.individual.scrape_candidate |
||||||
|
})[0] |
||||||
|
document.getElementById('selectVolume').value = this.individual.scrape_candidate |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('directory', { |
||||||
|
props: ['directory'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded' v-on:click="changeModal()"> |
||||||
|
<div class="stashr-badge_tl badge badge-pill badge-info border"></div> |
||||||
|
<div class="stashr-badge_br badge badge-pill border"></div> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-check_box p-1"> |
||||||
|
<a @click='changeChecked'> |
||||||
|
<i :class='checkedStatus'></i> |
||||||
|
<!-- |
||||||
|
<i class="text-info fa-2x fas fa-circle"></i> |
||||||
|
<i class="text-success fa-2x fas fa-check-circle"></i> |
||||||
|
--> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class="stashr-overlay_bottom w-100 center"> |
||||||
|
<span>[[ directory.scrape_directory ]]</span> |
||||||
|
</div> |
||||||
|
<div data-bs-toggle="modal" data-bs-target="#modalScrape"> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/folder.svg" /> |
||||||
|
<a class="stashr-poster_link"> |
||||||
|
<img class="w-100" :src="imageURL" loading="lazy"/> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
computed: { |
||||||
|
imageURL() { |
||||||
|
return JSON.parse(this.directory.scrape_json).filter(item => { |
||||||
|
return item['id'] == this.directory.scrape_candidate |
||||||
|
})[0]['image']['small_url'] |
||||||
|
}, |
||||||
|
checkedStatus() { |
||||||
|
string = `text-info fa-2x fas fa-circle` |
||||||
|
if(this.directory.scrape_add) { |
||||||
|
string = `text-success fa-2x fas fa-check-circle` |
||||||
|
} |
||||||
|
return string |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
changeModal() { |
||||||
|
app.changeModal(this.directory) |
||||||
|
}, |
||||||
|
changeChecked() { |
||||||
|
|
||||||
|
axios.put('{{ url_for('api.api_put_directories_edit', scrape_id='SCRAPEID') }}'.replace('SCRAPEID', this.directory.scrape_id), { |
||||||
|
data: { |
||||||
|
scrape_add: !this.directory.scrape_add, |
||||||
|
scrape_candidate: this.directory.scrape_candidate |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
console.log(res) |
||||||
|
if(res.data.status_code == 200) { |
||||||
|
this.directory.scrape_add = !this.directory.scrape_add |
||||||
|
// stashrToast('Updated Inforamtion', 'info') |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
data() { return { hover: false, scraped: false, } }, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('directories', { |
||||||
|
props: ['directories'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="mb-3 px-5"> |
||||||
|
<input type="text" v-model="search" class="form-control" placeholder="Search Folders..." /> |
||||||
|
</div> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center"> |
||||||
|
<directory |
||||||
|
v-for="directory in filteredList" |
||||||
|
v-bind:directory="directory" |
||||||
|
v-bind:key="directory.scrape_id" |
||||||
|
></directory> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
data() { return { search: '', } }, |
||||||
|
computed: { |
||||||
|
filteredList() { |
||||||
|
return this.directories.filter(directory => { |
||||||
|
return directory.scrape_directory.toLowerCase().includes(this.search.toLowerCase()) |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: {}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
directories: [], |
||||||
|
individual: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getDirectories(); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getDirectories() { |
||||||
|
axios.get('{{ url_for('api.api_get_directories') }}', { |
||||||
|
params: { |
||||||
|
offset: this.directories.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.directories.push(result) |
||||||
|
}); |
||||||
|
if(this.directories.length < res.data.number_of_total_results) { |
||||||
|
this.getDirectories() |
||||||
|
} |
||||||
|
} |
||||||
|
this.individual = this.directories[0] |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
rescanDirectories() { |
||||||
|
stashrToast('Rescanning Directories', 'info'); |
||||||
|
axios.post('{{ url_for('api.api_post_directories_scan') }}') |
||||||
|
.then(res =>{ |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
this.directories = []; |
||||||
|
this.getDirectories(); |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
changeModal(directory) { |
||||||
|
this.individual = directory |
||||||
|
}, |
||||||
|
addDirectories() { |
||||||
|
axios.post('{{ url_for('api.api_post_directories_add') }}') |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == 200) { |
||||||
|
stashrToast('Adding Directories') |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{{ emit_tep('scrape_page_script') }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,189 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('search_page_header') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<volumes v-bind:volumes='volumes' ref="volumes" v-on:do-search="getSearchResults"></volumes> |
||||||
|
<modals v-bind:volume='volume'></modals> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block modals %} |
||||||
|
{{ emit_tep('search_page_modals') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
{{ emit_tep('search_page_button_container') }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('modals', { |
||||||
|
props: [ |
||||||
|
'volume' |
||||||
|
], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="modalVolume" tabindex="-1" role="dialog" aria-labelledby="notesModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
[[ volume.name ]] ([[ volume.count_of_issues ]] Issues) |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<span v-html="volume.description"></span> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> |
||||||
|
<button id="subscribeButton" type="button" class="btn btn-success" data-bs-dismiss="modal" @click='subscribe'>Subscribe</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{ emit_tep('search_page_volume_modals') }} |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: { |
||||||
|
subscribe() { |
||||||
|
app.$refs.volumes.$children.find(child => {return child.$vnode.key == this.volume.id }).subscribe() |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[", "]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('volume-item', { |
||||||
|
props: ['volume'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded'> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> |
||||||
|
<a href="#">[[ volume.name ]]</a> |
||||||
|
</div> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> |
||||||
|
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalVolume" :data-volume_id=volume.id v-on:click="this.changeModal"> |
||||||
|
<img class="w-100" loading="lazy" :src="volume.image.medium_url"/> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
methods: { |
||||||
|
subscribe() { |
||||||
|
stashrToast('Adding to Library', 'info') |
||||||
|
axios.post('{{ url_for('api.api_post_single_volume', volume_id='VOLUMESTRING') }}'.replace('VOLUMESTRING', this.volume.id)) |
||||||
|
.then(res => { |
||||||
|
console.log(res) |
||||||
|
if (res.data.status_code == 200) { |
||||||
|
stashrToast('Added to Library', 'success'); |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
changeModal() { |
||||||
|
app.changeModal(this.volume) |
||||||
|
}, |
||||||
|
}, |
||||||
|
data() { return { hover: false }}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('volumes', { |
||||||
|
props: ['volumes'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<!-- |
||||||
|
<div class="form-group"> |
||||||
|
<input type="text" v-model="search" class="form-control" placeholder="Search Volumes..." /> |
||||||
|
</div> |
||||||
|
<button @click="$emit('do-search', search)">Search</button> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center"> |
||||||
|
<volume-item |
||||||
|
v-for="volume in filteredList" |
||||||
|
v-bind:volume="volume" |
||||||
|
v-bind:key="volume.id" |
||||||
|
></volume-item> |
||||||
|
</ul> |
||||||
|
--> |
||||||
|
<div class="input-group mb-3 px-5"> |
||||||
|
<input type="text" class="form-control" placeholder="Search Volumes..." aria-label="Recipient's username" aria-describedby="basic-addon2" v-model="search"> |
||||||
|
<div class="input-group-append"> |
||||||
|
<button class="btn btn-success" type="button" @click="$emit('do-search', search)"> |
||||||
|
Search |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center"> |
||||||
|
<volume-item |
||||||
|
v-for="volume in filteredList" |
||||||
|
v-bind:volume="volume" |
||||||
|
v-bind:key="volume.id" |
||||||
|
></volume-item> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
data() { return { search: '' }}, |
||||||
|
computed: { |
||||||
|
filteredList() { |
||||||
|
return this.volumes.filter(volume => { |
||||||
|
return volume.name.toLowerCase().includes(this.search.toLowerCase()) |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
volumes: [], |
||||||
|
volume: [] |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getSearchResults(query) { |
||||||
|
console.log('SEARCHING: ' + query) |
||||||
|
axios.get('{{ url_for('api.api_get_search') }}', { |
||||||
|
params: { |
||||||
|
query: query |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
this.volumes = res.data.results |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
changeModal(volume) { |
||||||
|
console.log('changing modal'); |
||||||
|
this.volume = volume |
||||||
|
}, |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
filteredList() { |
||||||
|
return this.volumes.filter(volume => { |
||||||
|
return volume.volume_name.toLowerCase().includes(this.search.toLowerCase()) |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{{ emit_tep('search_page_script') }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,61 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row w-100 m-0"> |
||||||
|
<div class="col-12 col-md-10 offset-md-1 bg-light rounded p-2"> |
||||||
|
<div class="row"> |
||||||
|
{% if current_user.role == 'admin' %} |
||||||
|
<div class="col-12 col-md-3 col-lg-2 p-3"> |
||||||
|
<ul class="nav flex-column nav-pills"> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('settings_app_page') %} active{% endif %}" href="{{ url_for('settings_app_page') }}"> |
||||||
|
<i class="fa fa-cogs"></i> |
||||||
|
Application |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('settings_directories_page') %} active{% endif %}" href="{{ url_for('settings_directories_page') }}"> |
||||||
|
<i class="fas fa-folder"></i> |
||||||
|
Directories |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('settings_mail_page') %} active{% endif %}" href="{{ url_for('settings_mail_page') }}"> |
||||||
|
<i class="fa fa-envelope"></i> |
||||||
|
Mail |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('settings_tasks_page') %} active{% endif %}" href="{{ url_for('settings_tasks_page') }}"> |
||||||
|
<i class="fa fa-tasks"></i> |
||||||
|
Tasks |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('settings_all_users_page') %} active{% endif %}" href="{{ url_for('settings_all_users_page') }}"> |
||||||
|
<i class="fa fa-users"></i> |
||||||
|
Users |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li class="nav-item"> |
||||||
|
<a class="nav-link{% if request.path == url_for('settings_plugins_page') %} active{% endif %}" href="{{ url_for('settings_plugins_page') }}"> |
||||||
|
<i class="fa fa-plug"></i> |
||||||
|
Plugins |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{{ emit_tep('settings_menu') }} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
<div class="col-12{% if current_user.role=='admin' %} col-md-9 col-lg-10{% endif %} my-2"> |
||||||
|
{% block settings_pane %}{% endblock %} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,103 @@ |
|||||||
|
{% extends "settings_page.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block settings_pane %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<users ref="users" v-bind:users="users"></users> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('user',{ |
||||||
|
props:['user'], |
||||||
|
template: ` |
||||||
|
<tr> |
||||||
|
<!-- |
||||||
|
:href="'{{ url_for('single_publisher_page', publisher_id='PUBLISHERID') }}'.replace('PUBLISHERID', publisher.publisher_id)" |
||||||
|
--> |
||||||
|
<td><a :href="'{{ url_for('settings_single_user_page', user_id='USERID') }}'.replace('USERID', user.id)">[[ user.id ]]</a></td> |
||||||
|
<td><a :href="'{{ url_for('settings_single_user_page', user_id='USERID') }}'.replace('USERID', user.id)">[[ user.username ]]</a></td> |
||||||
|
<td>[[ user.role ]]</td> |
||||||
|
<td>[[ user.age_rating_title.rating_long ]]</td> |
||||||
|
</tr> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('users', { |
||||||
|
props:['users'], |
||||||
|
template: ` |
||||||
|
<div class="row r-10 m-2"> |
||||||
|
<div class="col col-12"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-start"> |
||||||
|
<h2>Users</h2> |
||||||
|
</div> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-end"> |
||||||
|
<a type="button" class="btn btn-info" href="{{ url_for('settings_new_user_page') }}">New User</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="row"> |
||||||
|
<table class="table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scop="col">ID</th> |
||||||
|
<th scop="col">Username</th> |
||||||
|
<th scop="col">Role</th> |
||||||
|
<th scop="col">Age Rating</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody class="tablebody"> |
||||||
|
<user |
||||||
|
v-for="user in users" |
||||||
|
v-bind:user="user" |
||||||
|
v-bind:key="user.id" |
||||||
|
></user> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
users: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getUsers() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getUsers() { |
||||||
|
console.log('Getting Users'); |
||||||
|
axios.get('{{ url_for('api.api_get_users') }}', { |
||||||
|
params: { |
||||||
|
offset: this.users.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.users.push(result) |
||||||
|
}) |
||||||
|
if(this.users.length < res.data.number_of_total_results) { |
||||||
|
this.getUsers() |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,132 @@ |
|||||||
|
{% extends "settings_page.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block settings_pane %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<settings ref="settings" v-bind:settings="settings"></settings> |
||||||
|
<modals ref="modal" v-bind:settings="settings"></modals> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('settings', { |
||||||
|
props: ['settings'], |
||||||
|
template: ` |
||||||
|
<div class="row r-10 m-2"> |
||||||
|
<div class="col col-12"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-start"> |
||||||
|
<h2>Application</h2> |
||||||
|
</div> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-end"> |
||||||
|
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Modify Settings</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="row"> |
||||||
|
<table class="table table-striped"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<th scope="row">Server Port</th><td>[[ settings.server_port ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Open Registration</th><td>[[ settings.open_registration ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Comicvine API Key</th><td>[[ settings.comicvine_api_key ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Logging Level</th><td>[[ settings.log_level ]]</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modals', { |
||||||
|
props: ['settings'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="settingsModal" role="dialog" aria-labelledby="settingsModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Modify App Settings |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<form method="POST"> |
||||||
|
<div class="modal-body"> |
||||||
|
|
||||||
|
{{ settings_form.csrf_token }} |
||||||
|
|
||||||
|
<div class="mb-3"> |
||||||
|
{{ settings_form.server_port.label }} |
||||||
|
{{ settings_form.server_port(class_='form-control', placeholder=settings_form.server_port.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ settings_form.open_registration }} |
||||||
|
{{ settings_form.open_registration.label }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ settings_form.comicvine_api_key.label }} |
||||||
|
{{ settings_form.comicvine_api_key(class_='form-control', placeholder=settings_form.comicvine_api_key.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ settings_form.log_level.label }} |
||||||
|
{{ settings_form.log_level(class_='form-control') }} |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
{{ settings_form.update_app_button(class_='btn btn-success') }} |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: {}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
settings: [], |
||||||
|
mdset: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getSettings() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getSettings() { |
||||||
|
axios.get('{{ url_for('api.api_get_settings_single_section', section='app') }}') |
||||||
|
.then(res => { |
||||||
|
this.settings = res.data.results; |
||||||
|
document.getElementById('server_port').value = res.data.results.server_port; |
||||||
|
document.getElementById('open_registration').checked = res.data.results.open_registration; |
||||||
|
document.getElementById('comicvine_api_key').value = res.data.results.comicvine_api_key; |
||||||
|
document.getElementById('log_level').value = res.data.results.log_level; |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,150 @@ |
|||||||
|
{% extends "settings_page.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block settings_pane %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<settings ref="settings" v-bind:settings="settings"></settings> |
||||||
|
<modals ref="modal" v-bind:settings="settings"></modals> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('settings', { |
||||||
|
props: ['settings'], |
||||||
|
template: ` |
||||||
|
<div class="row r-10 m-2"> |
||||||
|
<div class="col col-12"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-start"> |
||||||
|
<h2>Directories</h2> |
||||||
|
</div> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-end"> |
||||||
|
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Modify Settings</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="row"> |
||||||
|
<table class="table table-striped"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<th scope="row">Temp Files</th><td>[[ settings.temp ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Comic Files</th><td>[[ settings.comics ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Log Files</th><td>[[ settings.log ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Backup Files</th><td>[[ settings.backup ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Plugin Files</th><td>[[ settings.plugins ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Image Files</th><td>[[ settings.images ]]</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modals', { |
||||||
|
props: ['settings'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="settingsModal" role="dialog" aria-labelledby="settingsModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Modify Directory Settings |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<form method="POST"> |
||||||
|
<div class="modal-body"> |
||||||
|
|
||||||
|
{{ directories_form.csrf_token }} |
||||||
|
|
||||||
|
<div class="mb-3"> |
||||||
|
{{ directories_form.temp_directory.label }} |
||||||
|
{{ directories_form.temp_directory(class_='form-control', placeholder=directories_form.temp_directory.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ directories_form.comics_directory.label }} |
||||||
|
{{ directories_form.comics_directory(class_='form-control', placeholder=directories_form.comics_directory.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ directories_form.log_directory.label }} |
||||||
|
{{ directories_form.log_directory(class_='form-control', placeholder=directories_form.log_directory.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ directories_form.backup_directory.label }} |
||||||
|
{{ directories_form.backup_directory(class_='form-control', placeholder=directories_form.backup_directory.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ directories_form.plugins_directory.label }} |
||||||
|
{{ directories_form.plugins_directory(class_='form-control', placeholder=directories_form.plugins_directory.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ directories_form.images_directory.label }} |
||||||
|
{{ directories_form.images_directory(class_='form-control', placeholder=directories_form.images_directory.label.text) }} |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
{{ directories_form.update_directory_button(class_='btn btn-success') }} |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: {}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
settings: [], |
||||||
|
mdset: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getSettings() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getSettings() { |
||||||
|
console.log('hooray') |
||||||
|
axios.get('{{ url_for('api.api_get_settings_single_section', section='directory') }}') |
||||||
|
.then(res => { |
||||||
|
this.settings = res.data.results |
||||||
|
document.getElementById('temp_directory').value = res.data.results.temp; |
||||||
|
document.getElementById('comics_directory').value = res.data.results.comics; |
||||||
|
document.getElementById('log_directory').value = res.data.results.log; |
||||||
|
document.getElementById('backup_directory').value = res.data.results.backup; |
||||||
|
document.getElementById('plugins_directory').value = res.data.results.plugins; |
||||||
|
document.getElementById('images_directory').value = res.data.results.images; |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,159 @@ |
|||||||
|
{% extends "settings_page.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block settings_pane %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<settings ref="settings" v-bind:settings="settings"></settings> |
||||||
|
<modals ref="modal" v-bind:settings="settings"></modals> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('settings', { |
||||||
|
props: ['settings'], |
||||||
|
template: ` |
||||||
|
<div class="row r-10 m-2"> |
||||||
|
<div class="col col-12"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-start"> |
||||||
|
<h2>Mail Settings</h2> |
||||||
|
</div> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-end"> |
||||||
|
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Modify Settings</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="row"> |
||||||
|
<table class="table table-striped"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ mail_form.mail_use.label.text }}</th><td>[[ settings.mail_use ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ mail_form.mail_username.label.text }}</th><td>[[ settings.mail_username ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ mail_form.mail_password.label.text }}</th><td>**********</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ mail_form.mail_default_sender.label.text }}</th><td>[[ settings.mail_default_sender ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ mail_form.mail_server.label.text }}</th><td>[[ settings.mail_server ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ mail_form.mail_port.label.text }}</th><td>[[ settings.mail_port ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">{{ mail_form.mail_use_ssl.label.text }}</th><td>[[ settings.mail_use_ssl ]]</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modals', { |
||||||
|
props: ['settings'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="settingsModal" role="dialog" aria-labelledby="settingsModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Modify Directory Settings |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<form method="POST"> |
||||||
|
<div class="modal-body"> |
||||||
|
|
||||||
|
{{ mail_form.csrf_token }} |
||||||
|
|
||||||
|
<div class="mb-3"> |
||||||
|
{{ mail_form.mail_use }} |
||||||
|
{{ mail_form.mail_use.label }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ mail_form.mail_username.label }} |
||||||
|
{{ mail_form.mail_username(class_='form-control', placeholder=mail_form.mail_username.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ mail_form.mail_password.label }} |
||||||
|
{{ mail_form.mail_password(type='password', class_='form-control', placeholder=mail_form.mail_password.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ mail_form.mail_default_sender.label }} |
||||||
|
{{ mail_form.mail_default_sender(class_='form-control', placeholder=mail_form.mail_default_sender.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ mail_form.mail_server.label }} |
||||||
|
{{ mail_form.mail_server(class_='form-control', placeholder=mail_form.mail_server.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ mail_form.mail_port.label }} |
||||||
|
{{ mail_form.mail_port(class_='form-control', placeholder=mail_form.mail_port.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ mail_form.mail_use_ssl }} |
||||||
|
{{ mail_form.mail_use_ssl.label }} |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
{{ mail_form.update_mail_button(class_='btn btn-success') }} |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: {}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
settings: [], |
||||||
|
mdset: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getSettings() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getSettings() { |
||||||
|
console.log('hooray') |
||||||
|
axios.get('{{ url_for('api.api_get_settings_single_section', section='mail') }}') |
||||||
|
.then(res => { |
||||||
|
this.settings = res.data.results |
||||||
|
|
||||||
|
document.getElementById('mail_use').checked = res.data.results.mail_use; |
||||||
|
document.getElementById('mail_username').value = res.data.results.mail_username; |
||||||
|
document.getElementById('mail_password').value = res.data.results.mail_password; |
||||||
|
document.getElementById('mail_default_sender').value = res.data.results.mail_from; |
||||||
|
document.getElementById('mail_server').value = res.data.results.mail_server; |
||||||
|
document.getElementById('mail_port').value = res.data.results.mail_port; |
||||||
|
document.getElementById('mail_use_ssl').checked = res.data.results.mail_use_ssl; |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,58 @@ |
|||||||
|
{% extends "settings_page.html" %} |
||||||
|
|
||||||
|
{% block settings_pane %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<div class="row r-10 m-2"> |
||||||
|
<div class="col col-12"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-start"> |
||||||
|
<h2>New User</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="row"> |
||||||
|
<form method="POST"> |
||||||
|
|
||||||
|
{{ new_user_form.csrf_token }} |
||||||
|
|
||||||
|
<div class="mb-3"> |
||||||
|
{{ new_user_form.username.label }} |
||||||
|
{{ new_user_form.username(class_='form-control', placeholder=new_user_form.username.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ new_user_form.email.label }} |
||||||
|
{{ new_user_form.email(class_='form-control', placeholder=new_user_form.email.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ new_user_form.password.label }} |
||||||
|
{{ new_user_form.password(type='password', class_='form-control', placeholder=new_user_form.password.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ new_user_form.confirm_password.label }} |
||||||
|
{{ new_user_form.confirm_password(type='password', class_='form-control', placeholder=new_user_form.confirm_password.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ new_user_form.role.label }} |
||||||
|
{{ new_user_form.role(class_='form-control') }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ new_user_form.age_rating.label }} |
||||||
|
{{ new_user_form.age_rating(class_='form-control') }} |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
{{ new_user_form.new_user_button(class_='btn btn-success') }} |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
{% endblock %} |
@ -0,0 +1,112 @@ |
|||||||
|
{% extends "settings_page.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block settings_pane %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<plugins ref="plugins" v-bind:plugins="plugins"></plugins> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('plugin',{ |
||||||
|
props:['plugin'], |
||||||
|
template: ` |
||||||
|
<tr> |
||||||
|
<th scope="row">[[ plugin.plugin_name ]]</th> |
||||||
|
<td>[[ plugin.plugin_description ]]</td> |
||||||
|
<td>[[ plugin.plugin_version ]]</td> |
||||||
|
<td>[[ plugin.plugin_author ]]</td> |
||||||
|
<td>[[ plugin.plugin_url ]]</td> |
||||||
|
<td>[[ plugin.plugin_license ]]</td> |
||||||
|
<td>[[ plugin.plugin_state ]]</td> |
||||||
|
<td><button type="button" class="btn" v-bind:class="pluginClass">[[ pluginAction ]]</button> </td> |
||||||
|
</tr> |
||||||
|
`, |
||||||
|
computed: { |
||||||
|
pluginClass() { |
||||||
|
let classname = 'btn-danger'; |
||||||
|
if(this.plugin.plugin_state == 'disabled') { |
||||||
|
classname = 'btn-success'; |
||||||
|
}; |
||||||
|
return classname; |
||||||
|
}, |
||||||
|
pluginAction() { |
||||||
|
let text = 'Disable'; |
||||||
|
if(this.plugin.plugin_state == 'disabled') { |
||||||
|
text = 'Enable'; |
||||||
|
}; |
||||||
|
return text; |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('plugins',{ |
||||||
|
props: ['plugins'], |
||||||
|
template: ` |
||||||
|
<div class="row r-10 m-2"> |
||||||
|
<div class="col col-12"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-start"> |
||||||
|
<h2>Plugins</h2> |
||||||
|
</div> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-end"> |
||||||
|
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Install Plugin</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="row"> |
||||||
|
<table class="table"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scop="col">Name</th> |
||||||
|
<th scop="col">Description</th> |
||||||
|
<th scop="col">Version</th> |
||||||
|
<th scop="col">Author</th> |
||||||
|
<th scop="col">URL</th> |
||||||
|
<th scop="col">License</th> |
||||||
|
<th scop="col">State</th> |
||||||
|
<th scop="col">Action</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody class="tablebody"> |
||||||
|
<plugin |
||||||
|
v-for="plugin in plugins" |
||||||
|
v-bind:plugin="plugin" |
||||||
|
v-bind:key="plugin.plugin_name" |
||||||
|
></plugin> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
plugins: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getPlugins() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getPlugins() { |
||||||
|
axios.get('{{ url_for('api.api_get_plugins') }}') |
||||||
|
.then(res => { |
||||||
|
this.plugins = res.data.results |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,248 @@ |
|||||||
|
{% extends "settings_page.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block settings_pane %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<user ref="user" v-bind:user="user"></user> |
||||||
|
<modals v-bind:user="user"></modals> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('user', { |
||||||
|
props: ['user'], |
||||||
|
template: ` |
||||||
|
<div class="row r-10 m-2"> |
||||||
|
<div class="col col-12"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-start"> |
||||||
|
<h2>User - [[ user.username ]]</h2> |
||||||
|
</div> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-end"> |
||||||
|
{% if current_user.id|int == user_id|int %} |
||||||
|
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#changePasswordModal">Change Password</button> |
||||||
|
{% endif %} |
||||||
|
{% if current_user.role == 'admin' %} |
||||||
|
{% if current_user.id|int != user_id|int %} |
||||||
|
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#resetPasswordModal">Reset Password</button> |
||||||
|
{% endif %} |
||||||
|
{% if user_id|int != 1 %} |
||||||
|
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteUserModal">Delete User</button> |
||||||
|
{% endif %} |
||||||
|
{% endif %} |
||||||
|
<br /> |
||||||
|
<button type="button" class="btn btn-info my-1" data-bs-toggle="modal" data-bs-target="#editUserModal">Edit Account</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="row"> |
||||||
|
<table class="table table-striped"> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<th scope="row">Username</th><td>[[ user.username ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Email</th><td>[[ user.email ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Role</th><td>[[ user.role ]]</td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Rating Allowed</th><td>[[ user.age_rating_title.rating_long ]]</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modals',{ |
||||||
|
props: ['user'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
{% if current_user.id|int == user_id|int %} |
||||||
|
<div class="modal" id="changePasswordModal" role="dialog" aria-labelledby="changePasswordModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Change Password |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<form method="POST"> |
||||||
|
<div class="modal-body"> |
||||||
|
|
||||||
|
{{ change_password_form.csrf_token }} |
||||||
|
|
||||||
|
<div class="mb-3"> |
||||||
|
{{ change_password_form.old_password.label }} |
||||||
|
{{ change_password_form.old_password(type='password', class_='form-control', placeholder=change_password_form.old_password.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ change_password_form.new_password.label }} |
||||||
|
{{ change_password_form.new_password(type='password', class_='form-control', placeholder=change_password_form.new_password.label.text) }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ change_password_form.confirm_password.label }} |
||||||
|
{{ change_password_form.confirm_password(type='password', class_='form-control', placeholder=change_password_form.confirm_password.label.text) }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
{{ change_password_form.update_password_button(class_='btn btn-success') }} |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
{% if current_user.role == 'admin' %} |
||||||
|
{% if current_user.id|int != user_id|int %} |
||||||
|
<div class="modal" id="resetPasswordModal" role="dialog" aria-labelledby="resetPasswordModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Change Password |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<form method="POST"> |
||||||
|
<div class="modal-body"> |
||||||
|
|
||||||
|
{{ reset_password_form.csrf_token }} |
||||||
|
|
||||||
|
<div class="mb-3"> |
||||||
|
{{ reset_password_form.password.label }} |
||||||
|
{{ reset_password_form.password(type='password', class_='form-control', placeholder=reset_password_form.password.label.text) }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
{{ reset_password_form.reset_password_button(class_='btn btn-success') }} |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
{% if user_id|int != 1 %} |
||||||
|
<div class="modal" id="deleteUserModal" role="dialog" aria-labelledby="deleteUserModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Delete User |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
Are you sure you want to delete user [[ user.username ]]? |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<form method="POST"> |
||||||
|
{{ delete_user_form.csrf_token }} |
||||||
|
{{ delete_user_form.delete_user_button(class_='btn btn-success') }} |
||||||
|
</form> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
{% endif %} |
||||||
|
<div class="modal" id="editUserModal" role="dialog" aria-labelledby="editUserModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Edit User |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<form method="POST"> |
||||||
|
<div class="modal-body"> |
||||||
|
|
||||||
|
{{ edit_user_form.csrf_token }} |
||||||
|
|
||||||
|
<div class="mb-3"> |
||||||
|
{{ edit_user_form.email.label }} |
||||||
|
{{ edit_user_form.email(class_='form-control', placeholder=edit_user_form.email.label.text) }} |
||||||
|
</div> |
||||||
|
{% if current_user.role == 'admin' %} |
||||||
|
<div class="mb-3"> |
||||||
|
{{ edit_user_form.role.label }} |
||||||
|
{{ edit_user_form.role(class_='form-control') }} |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
{{ edit_user_form.age_rating.label }} |
||||||
|
{{ edit_user_form.age_rating(class_='form-control') }} |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
{{ edit_user_form.edit_user_button(class_='btn btn-success') }} |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: {}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
user: [] |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getUser() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getUser() { |
||||||
|
console.log('getting user') |
||||||
|
console.log('{{ user_id }}') |
||||||
|
axios.get('{{ url_for('api.api_get_single_user', user_id=user_id) }}') |
||||||
|
.then(res => { |
||||||
|
this.user = res.data.results |
||||||
|
|
||||||
|
if(document.getElementById('email')){ |
||||||
|
document.getElementById('email').value = res.data.results.email; |
||||||
|
} |
||||||
|
if(document.getElementById('user_id')){ |
||||||
|
document.getElementById('user_id').value = res.data.results.id; |
||||||
|
} |
||||||
|
if(document.getElementById('reset_user_id')){ |
||||||
|
document.getElementById('reset_user_id').value = res.data.results.id; |
||||||
|
} |
||||||
|
if(document.getElementById('role')){ |
||||||
|
document.getElementById('role').value = res.data.results.role; |
||||||
|
} |
||||||
|
if(document.getElementById('age_rating')){ |
||||||
|
document.getElementById('age_rating').value = res.data.results.rating_allowed; |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,39 @@ |
|||||||
|
{% extends "settings_page.html" %} |
||||||
|
|
||||||
|
{% block settings_pane %} |
||||||
|
<div class="row r-10 m-2"> |
||||||
|
<div class="col col-12"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12 col-md-6 text-center text-md-start"> |
||||||
|
<h2>Tasks</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<hr /> |
||||||
|
<div class="row"> |
||||||
|
<table class="table table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th scope="col">TASK</th> |
||||||
|
<th scope="col"></th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
<tr> |
||||||
|
<th scope="row">Update New Releases</th><td><i class="fas fa-play"></i></td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Update New Release Subscriptions</th><td><i class="fas fa-play"></i></td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Clean Database</th><td><i class="fas fa-play"></i></td> |
||||||
|
</tr> |
||||||
|
<tr> |
||||||
|
<th scope="row">Restart Server</th><td><i class="fas fa-play"></i></td> |
||||||
|
</tr> |
||||||
|
{{ emit_tep('settings_page_tasks_task') }} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endblock %} |
@ -0,0 +1,527 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
<script src="//cdn.jsdelivr.net/npm/sortablejs@1.8.4/Sortable.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('single_collection_page_header', collection=collection) }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<collection-jumbo ref="jumbo" v-bind:collection='collection'></collection-jumbo> |
||||||
|
<issues ref="issues" v-bind:issues='issues'></issues> |
||||||
|
<collection-modals v-bind:collection='collection'></collection-modals> |
||||||
|
<issue-modals v-bind:issue='issue' v-bind:owner='collection.user.id'></issue-modals> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block modals %} |
||||||
|
{{ emit_tep('single_collection_page_modals', collection=collection) }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
{{ emit_tep('single_collection_page_button_container', collection=collection) }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.directive('sortable', { |
||||||
|
inserted (el, binding, vnode) { |
||||||
|
let options = binding.value; |
||||||
|
options.onUpdate = (e) => vnode.data.on.sorted(e); |
||||||
|
const sortable = Sortable.create(el, binding.value); |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('issue-modals', { |
||||||
|
props: ['issue', 'owner'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="modalAction" tabindex="-1" role="dialog" aria-labelledby="actionModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="exampleModalLongTitle"> |
||||||
|
[[ issue.volume.volume_name || 'something' ]] #[[ issue.issue_number || 'something' ]] |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body center"> |
||||||
|
<a :href="'/read/'+issue.issue_id" id="actionRead" class="btn btn-success my-1"> |
||||||
|
<i class="fas fa-book-open"></i> |
||||||
|
Read |
||||||
|
</a> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') or |
||||||
|
(current_user.role.lower() == 'patron') %} |
||||||
|
<a :href="'/api/downloads/'+issue.issu_id" id="actionDownload" class="btn btn-success my-1"> |
||||||
|
<i class="fas fa-download"></i> |
||||||
|
Download |
||||||
|
</a> |
||||||
|
{% endif %} |
||||||
|
<hr> |
||||||
|
<button @click="toggleRead" id="actionToggle" class="btn btn-info my-1" data-bs-dismiss="modal"> |
||||||
|
<i class="fas fa-check"></i> |
||||||
|
Toggle Read |
||||||
|
</button> |
||||||
|
<span v-if="{{ current_user.id }} == owner" > |
||||||
|
<button @click="removeFromCollection" id="actionRemove" class="btn btn-danger my-1" data-bs-dismiss="modal"> |
||||||
|
<i class="fas fa-minus"></i> |
||||||
|
Remove From Collection |
||||||
|
</button> |
||||||
|
<hr> |
||||||
|
<button @click="setCover" id="actionCover" class="btn btn-danger my-1" data-bs-dismiss="modal"> |
||||||
|
<i class="fas fa-portrait"></i> |
||||||
|
Set as Collection Cover |
||||||
|
</button> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{ emit_tep('single_collection_page_issue_modals', collection=collection) }} |
||||||
|
</div>`, |
||||||
|
methods: { |
||||||
|
toggleRead() { |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).toggleRead() |
||||||
|
}, |
||||||
|
removeFromCollection() { |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).removeFromCollection() |
||||||
|
}, |
||||||
|
setCover() { |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).setCover() |
||||||
|
}, |
||||||
|
}, |
||||||
|
data() { return { curuser: {{ current_user.id }} } }, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('collection-modals', { |
||||||
|
props: ['collection'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="modalInfo" tabindex="-1" role="dialog" aria-labelledby="infoModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="exampleModalLongTitle"> |
||||||
|
[[ this.collection.collection_name ]] |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
[[ this.collection.collection_description ]] |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% if collection.collection_user_id == current_user.id %} |
||||||
|
|
||||||
|
<div class="modal" id="modalEditCollection" tabindex="-1" role="dialog" aria-labelledby="editCollectionModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="exampleModalLongTitle"> |
||||||
|
<span id="collectionName">Edit Collection</span> |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<form> |
||||||
|
<div class="form-group"> |
||||||
|
<label for="collection_name">Collection Name:</label> |
||||||
|
<input id="collection_name" class="form-control" type="text" placeholder="Default input" :value="this.collection.collection_name"> |
||||||
|
</div> |
||||||
|
<div class="form-check"> |
||||||
|
<input class="form-check-input" type="checkbox" id="collection_public" :checked='this.collection.collection_public'> |
||||||
|
<label class="form-check-label" for="collection_public"> |
||||||
|
Make Collection Public |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div class="form-group"> |
||||||
|
<label for="comment">Description:</label> |
||||||
|
<textarea class="form-control" rows="3" id="collection_description" :value="this.collection.collection_description"></textarea> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
<button type="button" class="btn btn-success" data-bs-dismiss="modal" @click="editCollection">Update</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="modal" id="modalDeleteCollection" tabindex="-1" role="dialog" aria-labelledby="deleteCollectionModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="exampleModalLongTitle"> |
||||||
|
<span id="collectionName">Delete Collection</span> |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
Are you sure you want to delete this collection? |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" @click="removeCollection">Delete</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
{{ emit_tep('single_collection_page_collection_modals', collection=collection) }} |
||||||
|
</div> |
||||||
|
`, |
||||||
|
methods: { |
||||||
|
removeCollection() { |
||||||
|
app.removeCollection() |
||||||
|
}, |
||||||
|
editCollection() { |
||||||
|
app.editCollection() |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('collection-jumbo', { |
||||||
|
props: ['collection'], |
||||||
|
template: ` |
||||||
|
<div class="row w-100 m-0"> |
||||||
|
<div class='col-12 col-md-10 offset-md-1 p-2'> |
||||||
|
<div class='row'> |
||||||
|
<div class='col-12 col-md-2 p-2'> |
||||||
|
<div class='row'> |
||||||
|
<div class='col-10 offest-1'> |
||||||
|
<!-- START POSTER --> |
||||||
|
<div class="new-stashr_poster-wrapper border rounded"> |
||||||
|
<div class="stashr-poster_container rounded"> |
||||||
|
<img class="stashr-poster_background w-100" src="/static/assets/cover.svg" id="poster-bg"> |
||||||
|
<a class="rounded stashr-poster_link"> |
||||||
|
<img class="rounded stashr-poster_image w-100" id="lazy-img" v-bind:src="'/images/issues/'+collection.collection_cover_image+'.jpg'" onerror="this.src='/static/assets/cover.svg'"> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- END POSTER --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-12 col-md-10 p-2"> |
||||||
|
<div class="row w-100 bg-light rounded m-0 p-4"> |
||||||
|
<div class="col-12 col-md-10"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col text-center text-md-start"> |
||||||
|
<h1 class="stashr-series_title">[[ collection.collection_name ]]</h1> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row text-center text-md-start"> |
||||||
|
<div class="col-12"> |
||||||
|
<!-- START BADGES --> |
||||||
|
<span class="badge mx-1" :class="statusClass">[[ statusText ]]</span> |
||||||
|
<span class="badge bg-info mx-1">[[ collection.user.username ]]</span> |
||||||
|
{{ emit_tep('single_collection_page_badge_row', collection=collection) }} |
||||||
|
<!-- END BADGES --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<div class="col text-center text-md-start"> |
||||||
|
<!-- START BUTTON --> |
||||||
|
<button class="btn btn-primary m-1" type="button" data-bs-toggle="modal" data-bs-target="#modalInfo" :data-collection_name="collection.collection_name" :data-collection_description="collection.collection_description"> |
||||||
|
DESCRIPTION |
||||||
|
</button> |
||||||
|
{{ emit_tep('single_collection_page_button_row', collection=collection) }} |
||||||
|
<!-- END BUTTON --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="col-12 col-md-2 text-center text-md-right"> |
||||||
|
<!-- START ACTIONS --> |
||||||
|
{% if collection.collection_user_id == current_user.id %} |
||||||
|
<div class="dropdown"> |
||||||
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> |
||||||
|
Actions |
||||||
|
</button> |
||||||
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton"> |
||||||
|
<a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalEditCollection"> |
||||||
|
<i class="fas fa-edit"></i> |
||||||
|
Edit Collection |
||||||
|
</a> |
||||||
|
<a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalDeleteCollection"> |
||||||
|
<i class="fas fa-trash-alt"></i> |
||||||
|
Delete Collection |
||||||
|
</a> |
||||||
|
{{ emit_tep('single_collection_page_action_dropdown', collection=collection) }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
<!-- END ACTIONS --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div>`, |
||||||
|
computed: { |
||||||
|
statusClass() { |
||||||
|
let classname = 'bg-danger'; |
||||||
|
if (this.collection.collection_public) { |
||||||
|
classname = 'bg-success'; |
||||||
|
}; |
||||||
|
return classname; |
||||||
|
}, |
||||||
|
statusText() { |
||||||
|
let text = 'Private'; |
||||||
|
if (this.collection.collection_public) { |
||||||
|
text = 'Public'; |
||||||
|
} |
||||||
|
return text; |
||||||
|
} |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('issues', { |
||||||
|
props: ['issues'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center" v-sortable="$options.sortOptions" @sorted='handleSorted'> |
||||||
|
<issue |
||||||
|
v-for="issue in issues" |
||||||
|
v-bind:issue="issue" |
||||||
|
v-bind:key="issue.issue_id" |
||||||
|
></issue> |
||||||
|
</ul> |
||||||
|
</div>`, |
||||||
|
methods: { |
||||||
|
handleSorted(event) { |
||||||
|
app.handleSorted(event) |
||||||
|
}, |
||||||
|
}, |
||||||
|
sortOptions: { |
||||||
|
draggable: '.js-sortable-block', |
||||||
|
handle: '.js-drag-handle', |
||||||
|
delay: 300, |
||||||
|
delayOnTouchOnly: true |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('issue', { |
||||||
|
props: ['issue'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2 js-sortable-block' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded'> |
||||||
|
{% if collection.collection_user_id == current_user.id %} |
||||||
|
<div class="stashr-badge_tl badge rounded-pill bg-dark border js-drag-handle"> |
||||||
|
<i class="fas fa-arrows-alt"></i> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
<div class="stashr-badge_br badge rounded-pill bg-dark border"> |
||||||
|
<i class="fas fa-eye" :class="statusClass"></i> |
||||||
|
</div> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> |
||||||
|
<a href="#">[[ issue.volume.volume_name ]] #[[ issue.issue_number ]]</a> |
||||||
|
</div> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> |
||||||
|
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalAction" @click="changeModal"> |
||||||
|
<img class="w-100" loading="lazy" v-bind:src="'/images/issues/'+ issue.issue_id +'.jpg'"/> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
computed: { |
||||||
|
statusClass() { |
||||||
|
let classname = 'text-danger'; |
||||||
|
if (this.issue.read_status[0].read_status) { |
||||||
|
classname = 'text-success'; |
||||||
|
} |
||||||
|
return classname; |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
setCover() { |
||||||
|
app.setCover() |
||||||
|
}, |
||||||
|
toggleRead() { |
||||||
|
axios.put('{{ url_for('api.api_put_single_issue', issue_id='ISSUEID') }}'.replace('ISSUEID', this.issue.issue_id), { |
||||||
|
data: { |
||||||
|
read_status: !this.issue.read_status[0].read_status |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Toggled Read Status', 'success'); |
||||||
|
this.issue.read_status[0].read_status = !this.issue.read_status[0].read_status |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
removeFromCollection() { |
||||||
|
app.removeFromCollection() |
||||||
|
}, |
||||||
|
changeModal() { |
||||||
|
app.changeModal(this.issue) |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { return { hover: false } }, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
collection: [], |
||||||
|
issues: [], |
||||||
|
issue: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getCollection() |
||||||
|
this.getIssues() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
|
||||||
|
getCollection() { |
||||||
|
console.log('gettingcollection') |
||||||
|
console.log('{{ url_for('api.api_get_single_collection', collection_slug=collection_slug) }}') |
||||||
|
axios.get('{{ url_for('api.api_get_single_collection', collection_slug=collection_slug) }}') |
||||||
|
.then(res => { |
||||||
|
this.collection = res.data.results |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
|
||||||
|
getIssues() { |
||||||
|
axios.get('{{ url_for('api.api_get_collection_all_issues', collection_slug=collection_slug) }}', { |
||||||
|
params: { |
||||||
|
offset: this.issues.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.issues.push(result) |
||||||
|
}) |
||||||
|
if(this.issues.length < res.data.number_of_total_results) { |
||||||
|
this.getIssues() |
||||||
|
} |
||||||
|
} |
||||||
|
this.issue = this.issues[0] |
||||||
|
}) |
||||||
|
|
||||||
|
}, |
||||||
|
|
||||||
|
changeModal(issue) { |
||||||
|
this.issue = issue |
||||||
|
}, |
||||||
|
|
||||||
|
editCollection() { |
||||||
|
new_name = document.getElementById('collection_name').value; |
||||||
|
new_public = document.getElementById('collection_public').checked; |
||||||
|
new_description = document.getElementById('collection_description').value; |
||||||
|
axios.put('{{ url_for('api.api_put_single_collection', collection_slug=collection_slug) }}', { |
||||||
|
data: { |
||||||
|
collection_name: new_name, |
||||||
|
collection_public: new_public, |
||||||
|
collection_description: new_description, |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code=='200'){ |
||||||
|
stashrToast('Updated Collection', 'success') |
||||||
|
this.collection.collection_name = new_name; |
||||||
|
this.collection.collection_description = new_description; |
||||||
|
this.collection.collection_public = new_public; |
||||||
|
}else{ |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
|
||||||
|
setCover() { |
||||||
|
axios.put('{{ url_for('api.api_put_single_collection', collection_slug='COLLECTIONID') }}'.replace('COLLECTIONID', this.collection.collection_id), { |
||||||
|
data: { |
||||||
|
collection_cover_image: this.issue.issue_id |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Cover Image Set', 'success'); |
||||||
|
app.$refs.jumbo.collection.collection_cover_image = this.issue.issue_id; |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
|
||||||
|
handleSorted(event) { |
||||||
|
axios.put('{{ url_for('api.api_put_single_collection', collection_slug='COLLECTIONID') }}'.replace('COLLECTIONID', this.collection.collection_id), { |
||||||
|
data: { |
||||||
|
old_index: event.oldIndex, |
||||||
|
new_index: event.newIndex |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code== '200') { |
||||||
|
stashrToast('Updated Order', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
|
||||||
|
removeFromCollection() { |
||||||
|
axios.delete('{{ url_for('api.api_delete_collection_issue', collection_slug=collection_slug, issue_id='ISSUEID') }}'.replace('ISSUEID', this.issue.issue_id)) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Removed from Collection', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
this.issues = this.issues.filter(issue => issue.issue_id !== this.issue.issue_id) |
||||||
|
// this.issues = this.issues.filter(remove => this.remove.issue_id !== app.issue.issue_id) |
||||||
|
}, |
||||||
|
|
||||||
|
removeCollection() { |
||||||
|
console.log('removing collection') |
||||||
|
axios.delete('{{ url_for('api.api_delete_single_collection', collection_slug=collection_slug) }}') |
||||||
|
.then(res =>{ |
||||||
|
window.location.replace('{{ url_for('all_collections_page') }}') |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
|
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{{ emit_tep('single_collection_page_script', collection=collection) }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,200 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('single_publisher_page_header', publisher_id=publisher_id) }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<publisher-jumbo v-bind:publisher="publisher"></publisher-jumbo> |
||||||
|
<volumes v-bind:volumes='volumes'></volumes> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block modals %} |
||||||
|
{{ emit_tep('single_publisher_page_modals', publisher_id=publisher_id) }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
{{ emit_tep('single_publisher_page_button_container', publisher_id=publisher_id) }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('volumes', { |
||||||
|
props: ['volumes'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="mb-3 px-5"> |
||||||
|
<input type="text" v-model="search" class="form-control" placeholder="Search Publisher..." /> |
||||||
|
</div> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center"> |
||||||
|
<volume-item |
||||||
|
v-for="volume in filteredList" |
||||||
|
v-bind:volume="volume" |
||||||
|
v-bind:key="volume.volume_id" |
||||||
|
></volume-item> |
||||||
|
</ul> |
||||||
|
<!-- |
||||||
|
<i class="text-primary fas fa-spinner fa-spin fa-3x" v-if="loading"></i> |
||||||
|
--> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
data() { return { loading: true, search: '', } }, |
||||||
|
computed: { |
||||||
|
filteredList() { |
||||||
|
return this.volumes.filter(volume => { |
||||||
|
return volume.volume_name.toLowerCase().includes(this.search.toLowerCase()) |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('volume-item', { |
||||||
|
props: ['volume'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded'> |
||||||
|
<div class="stashr-badge_tl badge rounded-pill bg-info border">[[ volume.age_rating[0].rating_short ]]</div> |
||||||
|
<div class="stashr-badge_tr badge rounded-pill bg-primary border">[[ volume.volume_have ]]/[[ volume.volume_total ]]</div> |
||||||
|
<div class="stashr-badge_br badge rounded-pill border" :class="statusClass">[[ statusWord ]]</div> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> |
||||||
|
<a href="#">[[ volume.volume_name ]]</a> |
||||||
|
</div> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> |
||||||
|
<a class="stashr-poster_link" :href="'/volumes/'+volume.volume_slug"> |
||||||
|
<img class="w-100" loading="lazy" v-bind:src="'/images/volumes/'+volume.volume_id+'.jpg'" @error="$event.target.src=volume.volume_image_med"/> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
computed: { |
||||||
|
statusClass() { |
||||||
|
let classname = 'bg-danger'; |
||||||
|
if(this.volume.volume_status) { |
||||||
|
classname = 'bg-success'; |
||||||
|
}; |
||||||
|
return classname; |
||||||
|
}, |
||||||
|
statusWord() { |
||||||
|
let status = 'ENDED'; |
||||||
|
if(this.volume.volume_status) { |
||||||
|
status = 'ONGOING'; |
||||||
|
}; |
||||||
|
return status; |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { return { hover: false } }, |
||||||
|
delimiters: ["[[","]]"], |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('publisher-jumbo', { |
||||||
|
props: ['publisher'], |
||||||
|
template: ` |
||||||
|
<div class="row w-100 m-0"> |
||||||
|
<div class='col-12 col-md-10 offset-md-1 p-2'> |
||||||
|
<div class='row'> |
||||||
|
<div class='col-12 col-md-2 p-2'> |
||||||
|
<div class='row'> |
||||||
|
<div class='col-10 offset-1'> |
||||||
|
<!-- START POSTER --> |
||||||
|
<div class="new-stashr_poster-wrapper border rounded"> |
||||||
|
<div class="stashr-poster_container rounded"> |
||||||
|
<img class="stashr-poster_background w-100" src="/static/assets/cover.svg" id="poster-bg"> |
||||||
|
<a class="rounded stashr-poster_link"> |
||||||
|
<img class="rounded stashr-poster_image w-100" id="lazy-img" v-bind:src="publisher.publisher_image"> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- END POSTER --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-12 col-md-10 p-2"> |
||||||
|
<div class="row w-100 bg-light rounded m-0 p-4"> |
||||||
|
<div class="col-12 col-md-10"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col text-center text-md-start"> |
||||||
|
<h1 class="stashr-series_title">[[ publisher.publisher_name ]]</h1> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
computed: {}, |
||||||
|
methods: {}, |
||||||
|
created() {}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
publisher: [], |
||||||
|
volumes: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getPublisher(); |
||||||
|
this.getVolumes(); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getPublisher() { |
||||||
|
axios.get('{{ url_for('api.api_get_single_publisher', publisher_id=publisher_id) }}') |
||||||
|
.then(res => { |
||||||
|
console.log(res) |
||||||
|
if(res.data.status_code == 200) { |
||||||
|
console.log('woooo') |
||||||
|
this.publisher = res.data.results |
||||||
|
} else { |
||||||
|
displayToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log('boo-'+err)) |
||||||
|
}, |
||||||
|
getVolumes() { |
||||||
|
console.log('getting vols') |
||||||
|
axios.get('{{ url_for('api.api_get_single_publisher_volumes', publisher_id=publisher_id) }}', { |
||||||
|
params: { |
||||||
|
offset: this.volumes.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
// console.log(res) |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.volumes.push(result) |
||||||
|
this.publisher = this.volumes[0].publisher |
||||||
|
}); |
||||||
|
if(this.volumes.length < res.data.number_of_total_results) { |
||||||
|
this.getVolumes(); |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log('ERROR: ' + err)) |
||||||
|
// this.publisher = this.volumes[0].publisher |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
{{ emit_tep('single_publisher_page_script', publisher_id=publisher_id) }} |
||||||
|
|
||||||
|
{% endblock %} |
@ -0,0 +1,903 @@ |
|||||||
|
{% extends "base.html" %} |
||||||
|
|
||||||
|
{% block header_script_files %} |
||||||
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block header %} |
||||||
|
{{ emit_tep('single_volume_page_header', volume_id=volume_id) }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block content %} |
||||||
|
|
||||||
|
<div id="app"> |
||||||
|
<volume-jumbo v-bind:volume='volume' ref="volume"></volume-jumbo> |
||||||
|
<issues ref="issues" v-bind:issues='issues'></issues> |
||||||
|
<modals-volume v-bind:volume='volume' v-bind:ratings='ratings'></modals-volume> |
||||||
|
<modals-issue v-bind:issue='issue' v-bind:collections='collections'></modals-issue> |
||||||
|
</div> |
||||||
|
|
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block modals %} |
||||||
|
{{ emit_tep('single_volume_page_modals', volume_id=volume_id) }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block button_container %} |
||||||
|
{{ emit_tep('single_volume_page_button_container', volume_id=volume_id) }} |
||||||
|
{% endblock %} |
||||||
|
|
||||||
|
{% block script %} |
||||||
|
|
||||||
|
Vue.component('collection-option', { |
||||||
|
props: ['collection'], |
||||||
|
template: ` |
||||||
|
<option :value="collection.collection_id">[[ collection.collection_name ]]</option> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('rating-option', { |
||||||
|
props: ['rating'], |
||||||
|
template: ` |
||||||
|
<option :value="rating.rating_value">[[ rating.rating_long ]]</option> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modals-issue', { |
||||||
|
props: [ |
||||||
|
'issue', |
||||||
|
'collections' |
||||||
|
], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="modalCollection" tabindex="-1" role="dialog" aria-labelledby="notesModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Add to Collection |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<div class="form-group"> |
||||||
|
<label for="collectionName">Add To Collection</label> |
||||||
|
<select ref="collectionselection" class="form-control" id="collectionName" @change='changeCollection'> |
||||||
|
<option value='0'>+ Create New Collection</option> |
||||||
|
<collection-option |
||||||
|
v-for='collection in collections' |
||||||
|
v-bind:collection='collection' |
||||||
|
v-bind:key='collection.collection_id' |
||||||
|
></collection-option> |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<div id="newCollectionName" class="form-group" v-if="this.collection_id == '0'"> |
||||||
|
<label for="newCollection">New Collection Name</label> |
||||||
|
<input ref="collectionName" type="email" class="form-control" id="newCollection" placeholder="New Collection Name"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
<button id="addToCollectionButton" type="button" class="btn btn-success" data-bs-dismiss="modal" @click='addToCollection'>Add to Collection</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') %} |
||||||
|
<div class="modal" id="modalDelete" tabindex="-1" role="dialog" aria-labelledby="deleteModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Delete Issue |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<form id="deleteIssueForm" action="{{ url_for('single_volume_page', volume_id=volume_id) }}" method="POST"> |
||||||
|
<div class="modal-body"> |
||||||
|
Are you sure you want to delete [[ issue.volume.volume_name ]] #[[ issue.issue_number ]] |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" @click="deleteIssue">Delete</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="modal" id="modalUpload" tabindex="-1" role="dialog" aria-labelledby="deleteModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Upload File |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<form> |
||||||
|
<div class="input-group mb-3"> |
||||||
|
<input id="fileupload" type="file" accept=".cbz, .cbr, .cb7, .cbt" name="comic" @change='uploadFile' required="required" class="form-control"> |
||||||
|
</div> |
||||||
|
|
||||||
|
</form> |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
<div class="modal" id="modalAction" tabindex="-1" role="dialog" aria-labelledby="actionModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
<p>[[ issue.volume.volume_name ]] #[[ issue.issue_number ]]</p> |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body center"> |
||||||
|
<span v-if="issue.issue_file_status"> |
||||||
|
<a id="actionHaveRead" class="btn btn-success my-1" :href="'/read/'+issue.issue_id"> |
||||||
|
<i class="fas fa-book-open"></i> |
||||||
|
Read |
||||||
|
</a> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') or |
||||||
|
(current_user.role.lower() == 'patron') %} |
||||||
|
<a id="actionHaveDownload" class="btn btn-success my-1" :href="'/api/downloads/'+issue.issue_id"> |
||||||
|
<i class="fas fa-download"></i> |
||||||
|
Download |
||||||
|
</a> |
||||||
|
{% endif %} |
||||||
|
<hr /> |
||||||
|
</span> |
||||||
|
<span v-if="!issue.issue_file_status"> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') %} |
||||||
|
<button id="actionMissingUpload" class="btn btn-success my-1" type="button" data-bs-dismiss="modal" data-bs-toggle="modal" data-bs-target="#modalUpload"> |
||||||
|
<i class="fas fa-cloud-upload-alt"></i> |
||||||
|
Upload to Server |
||||||
|
</button> |
||||||
|
<hr /> |
||||||
|
{% endif %} |
||||||
|
</span> |
||||||
|
<span v-if="issue.issue_file_status"> |
||||||
|
<button id="actionHaveList" class="btn btn-info my-1" data-bs-dismiss="modal" v-on:click="addToReadingList"> |
||||||
|
<i class="fas fa-plus"></i> |
||||||
|
Add to Reading List |
||||||
|
</button> |
||||||
|
<button id="actionHaveCollection" class="btn btn-info my-1" data-bs-dismiss="modal" data-bs-toggle="modal" data-bs-target="#modalCollection"> |
||||||
|
<i class="fas fa-plus"></i> |
||||||
|
Add to Collection |
||||||
|
</button> |
||||||
|
<hr /> |
||||||
|
</span> |
||||||
|
<span> |
||||||
|
<button id="actionHaveToggleRead" class="btn btn-info my-1" v-on:click="toggleReadStatus" data-bs-dismiss="modal"> |
||||||
|
<i class="fas fa-check"></i> |
||||||
|
Toggle Read |
||||||
|
</button> |
||||||
|
<button id="actionHaveToggleOwned" class="btn btn-info my-1" v-on:click="toggleOwnedStatus" data-bs-dismiss="modal"> |
||||||
|
<i class="fas fa-book"></i> |
||||||
|
Toggle Owned |
||||||
|
</button> |
||||||
|
</span> |
||||||
|
<span v-if="issue.issue_file_status"> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') %} |
||||||
|
<hr /> |
||||||
|
<button id="actionHaveDelete" class="btn btn-danger my-1" type="button" data-bs-dismiss="modal" data-bs-toggle="modal" data-bs-target="#modalDelete"> |
||||||
|
<i class="fas fa-trash-alt"></i> |
||||||
|
Delete |
||||||
|
</button> |
||||||
|
{% endif %} |
||||||
|
</span> |
||||||
|
<span v-if="issue.issue_file_status"> |
||||||
|
{{ emit_tep("single_volume_page_issues_modal_have", volume_id=volume_id) }} |
||||||
|
</span> |
||||||
|
<span v-if="!issue.issue_file_status"> |
||||||
|
{{ emit_tep("single_volume_page_issues_modal_missing", volume_id=volume_id) }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
// collections: [], |
||||||
|
collection_id: 0, |
||||||
|
file: '', |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
addToReadingList() { |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).addToReadingList() |
||||||
|
}, |
||||||
|
addToCollection() { |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).addToCollection() |
||||||
|
}, |
||||||
|
toggleReadStatus() { |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).toggleRead() |
||||||
|
}, |
||||||
|
toggleOwnedStatus() { |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).toggleOwned() |
||||||
|
}, |
||||||
|
changeCollection() { |
||||||
|
this.collection_id = this.$refs.collectionselection.value |
||||||
|
}, |
||||||
|
deleteIssue() { |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).deleteIssue() |
||||||
|
}, |
||||||
|
addToCollection() { |
||||||
|
if(this.collection_id == '0') { |
||||||
|
axios.post('{{ url_for('api.api_post_all_collections') }}', { |
||||||
|
data: { |
||||||
|
collection_name: this.$refs.collectionName.value, |
||||||
|
issue_id: this.issue.issue_id |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if (res.data.status_code == '200') { |
||||||
|
app.refreshCollections(); |
||||||
|
stashrToast('Collection Created', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
} else { |
||||||
|
axios.post('{{ url_for('api.api_post_single_collection', collection_slug='COLLECTIONID') }}'.replace('COLLECTIONID', this.$refs.collectionselection.value), { |
||||||
|
data: { |
||||||
|
issue_id: this.issue.issue_id |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Added to Collection', 'success'); |
||||||
|
} else { |
||||||
|
stashrToast(response.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
} |
||||||
|
}, |
||||||
|
uploadFile(){ |
||||||
|
let formData = new FormData(); |
||||||
|
var imagefile = document.querySelector('#fileupload'); |
||||||
|
formData.append("file", imagefile.files[0]); |
||||||
|
bootstrap.Modal.getInstance(document.getElementById('modalUpload')).hide() |
||||||
|
stashrToast('Uploading Comic', 'info') |
||||||
|
axios.post('{{ url_for('api.api_post_single_issue', issue_id='ISSUEID') }}'.replace('ISSUEID', this.issue.issue_id), formData, { |
||||||
|
headers: { |
||||||
|
'Content-Type': 'multipart/form-data' |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == 200) { |
||||||
|
stashrToast('Issue Uploaded', 'success') |
||||||
|
app.$refs.issues.$children.find(child => { return child.$vnode.key == this.issue.issue_id }).toggleFile() |
||||||
|
app.refreshVolumeInfo() |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => { |
||||||
|
stashrToast(err, 'error') |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('modals-volume', { |
||||||
|
props: [ |
||||||
|
'issue', |
||||||
|
'volume', |
||||||
|
'ratings' |
||||||
|
], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<div class="modal" id="modalInfo" tabindex="-1" role="dialog" aria-labelledby="infoModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="exampleModalLongTitle"> |
||||||
|
[[ volume.volume_name ]] |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<span v-html="volume.volume_description"></span> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') %} |
||||||
|
<div class="modal" id="modalRemove" tabindex="-1" role="dialog" aria-labelledby="removeModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="exampleModalLongTitle"> |
||||||
|
Remove [[ volume.volume_name ]] |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<p>Do you want to remove [[ volume.volume_name ]] from your library?</p> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" @click="removeFromLibrary">Remove</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="modal" id="modalRatingEdit" tabindex="-1" role="dialog" aria-labelledby="ratingEditModal" aria-hidden="true"> |
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title" id="notesModalTitle"> |
||||||
|
Edit Rating |
||||||
|
</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<form id="editRatingForm" action="{{ url_for('single_volume_page', volume_id=volume_id) }}" method="POST"> |
||||||
|
<div class="modal-body"> |
||||||
|
<select class="form-control" ref='ratingselection'> |
||||||
|
<rating-option |
||||||
|
v-for="rating in ratings" |
||||||
|
v-bind:rating='rating' |
||||||
|
v-bind:key='rating.rating_id' |
||||||
|
></rating-option> |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> |
||||||
|
<button type="button" class="btn btn-success" data-bs-dismiss="modal" @click="updateVolumeRating">Update</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{% endif %} |
||||||
|
</div> |
||||||
|
`, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
collections: [], |
||||||
|
collection_id: 0, |
||||||
|
file: '', |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
removeFromLibrary() { |
||||||
|
axios.delete('{{ url_for('api.api_delete_single_volume', volume_id=volume_id) }}') |
||||||
|
.then(res => { |
||||||
|
window.location.replace("{{ url_for('all_volumes_page') }}"); |
||||||
|
}) |
||||||
|
.catch() |
||||||
|
}, |
||||||
|
updateVolumeRating() { |
||||||
|
axios.put('{{ url_for('api.api_put_single_volume', volume_id=volume_id) }}', { |
||||||
|
data: { |
||||||
|
volume_age_rating: this.$refs.ratingselection.value |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
app.$refs.volume.volume.age_rating = app.ratings.filter(child => child.rating_value == this.$refs.ratingselection.value ) |
||||||
|
stashrToast('Updated Comic Rating', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('volume-jumbo', { |
||||||
|
props: ['volume'], |
||||||
|
template: ` |
||||||
|
<div class="row w-100 m-0"> |
||||||
|
<div class='col-12 col-md-10 offset-md-1 p-2'> |
||||||
|
<div class='row'> |
||||||
|
<div class='col-12 col-md-2 p-2'> |
||||||
|
<div class='row'> |
||||||
|
<div class='col-10 offset-1'> |
||||||
|
<!-- START POSTER --> |
||||||
|
<div class="new-stashr_poster-wrapper border rounded"> |
||||||
|
<div class="stashr-poster_container rounded"> |
||||||
|
<img class="stashr-poster_background w-100" src="/static/assets/cover.svg" id="poster-bg"> |
||||||
|
<a class="rounded stashr-poster_link"> |
||||||
|
<img class="rounded stashr-poster_image w-100" id="lazy-img" v-bind:src="'/images/volumes/'+volume.volume_id+'.jpg'" @error="$event.target.src=volume.volume_image_med"> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- END POSTER --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-12 col-md-10 p-2"> |
||||||
|
<div class="row w-100 bg-light rounded m-0 p-4"> |
||||||
|
<div class="col-12 col-md-10"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col text-center text-md-start"> |
||||||
|
<h1 class="stashr-series_title">[[ volume.volume_name ]]</h1> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row text-center text-md-start"> |
||||||
|
<div class="col-12"> |
||||||
|
<!-- START BADGES --> |
||||||
|
<span class="badge mx-1" v-bind:class="volumeStatus">[[ statusWord ]]</span> |
||||||
|
<span class="badge bg-info mx-1">[[ volume.age_rating[0].rating_long ]]</span> |
||||||
|
<span class="badge bg-info mx-1">[[ publisherName ]]</span> |
||||||
|
<span class="badge bg-info mx-1">[[ volume.volume_year ]]</span> |
||||||
|
<br> |
||||||
|
<span class="badge bg-primary mx-1">Digital: [[ volume.volume_have ]]/[[ volume.volume_total ]]</span> |
||||||
|
<span class="badge bg-primary mx-1">Physical: [[ ownedNumber ]]/[[ volume.volume_total ]]</span> |
||||||
|
<span class="badge bg-primary mx-1">Read: [[ readNumber ]]/[[ volume.volume_total ]]</span> |
||||||
|
<a class="badge bg-primary mx-1" :href="volume.volume_url" target="new">ComicVine</a> |
||||||
|
<br> |
||||||
|
{{ emit_tep('single_volume_page_badge_row', volume_id=volume_id) }} |
||||||
|
<!-- END BADGES --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<div class="col text-center text-md-start"> |
||||||
|
<!-- START BUTTON --> |
||||||
|
<button class="btn btn-primary m-1" type="button" data-bs-toggle="modal" data-bs-target="#modalInfo"> |
||||||
|
INFORMATION |
||||||
|
</button> |
||||||
|
{{ emit_tep('single_volume_page_button_row', volume_id=volume_id) }} |
||||||
|
<!-- END BUTTON --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="col-12 col-md-2 text-center text-md-right"> |
||||||
|
<!-- START NEW ACTIONS --> |
||||||
|
<div class="dropdown"> |
||||||
|
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="actionMenu" data-bs-toggle="dropdown" aria-expanded="false"> |
||||||
|
Actions |
||||||
|
</a> |
||||||
|
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="actionMenu"> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') %} |
||||||
|
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalRatingEdit"> |
||||||
|
<i class="far fa-sticky-note mx-1"></i> |
||||||
|
Edit Rating |
||||||
|
</a></li> |
||||||
|
<li><a class="dropdown-item" v-on:click="refreshVolume"> |
||||||
|
<i class="fas fa-sync-alt mx-1"></i> |
||||||
|
Refresh |
||||||
|
</a></li> |
||||||
|
<li><a class="dropdown-item" v-on:click="toggleStatus"> |
||||||
|
<span v-if="volume.volume_status"> |
||||||
|
<i class="fas fa-stop mx-1"></i> |
||||||
|
Mark Ended |
||||||
|
</span> |
||||||
|
<span v-else> |
||||||
|
<i class="fas fa-play mx-1"></i> |
||||||
|
Mark Ongoing |
||||||
|
</span> |
||||||
|
</a></li> |
||||||
|
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalRemove"> |
||||||
|
<i class="fas fa-trash-alt mx-1"></i> |
||||||
|
Remove From Library |
||||||
|
</a></li> |
||||||
|
{% endif %} |
||||||
|
{{ emit_tep("single_volume_page_action_dropdown", volume_id=volume_id) }} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<!-- END NEW ACTIONS --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
computed: { |
||||||
|
publisherName() { |
||||||
|
let pub_name = 'Not Defined'; |
||||||
|
try { |
||||||
|
pub_name = this.volume.publisher.publisher_name; |
||||||
|
} finally { |
||||||
|
return pub_name; |
||||||
|
} |
||||||
|
}, |
||||||
|
ownedNumber() { |
||||||
|
var read_issues = app.issues.filter(issue => issue.owned_status[0].owned_status == 1) |
||||||
|
return read_issues.length |
||||||
|
}, |
||||||
|
readNumber() { |
||||||
|
var read_issues = app.issues.filter(issue => issue.read_status[0].read_status == 1) |
||||||
|
return read_issues.length |
||||||
|
}, |
||||||
|
volumeNote() { |
||||||
|
let note = '' |
||||||
|
try { |
||||||
|
note = this.volume.notes.volume_note; |
||||||
|
} finally { |
||||||
|
return note; |
||||||
|
}; |
||||||
|
}, |
||||||
|
volumeStatus() { |
||||||
|
let classname = 'bg-danger'; |
||||||
|
if (this.volume.volume_status) { |
||||||
|
classname = 'bg-success' |
||||||
|
}; |
||||||
|
return classname; |
||||||
|
}, |
||||||
|
statusWord() { |
||||||
|
let status = 'Ended'; |
||||||
|
if(this.volume.volume_status) { |
||||||
|
status = 'Ongoing'; |
||||||
|
}; |
||||||
|
return status; |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
toggleStatus() { |
||||||
|
axios.put('{{ url_for('api.api_put_single_volume', volume_id=volume_id) }}', { |
||||||
|
data: { |
||||||
|
volume_status: !this.volume.volume_status |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Status Updated', 'success') |
||||||
|
this.volume.volume_status = !this.volume.volume_status |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
refreshVolume() { |
||||||
|
app.refreshVolume() |
||||||
|
}, |
||||||
|
deleteSubscription() { |
||||||
|
axios.delete('{{ url_for('api.api_delete_single_volume', volume_id=volume_id) }}') |
||||||
|
.then(function(response) { |
||||||
|
window.location.replace("{{ url_for('all_volumes_page') }}"); |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
}, |
||||||
|
created() { |
||||||
|
document.title = 'Stashr - ' + this.volume.volume_name; |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('issue-item', { |
||||||
|
props: ['issue'], |
||||||
|
template: ` |
||||||
|
<li class='stashr-cover_size m-2' |
||||||
|
@mouseover="hover = true" |
||||||
|
@mouseleave="hover = false" |
||||||
|
> |
||||||
|
<div class='stashr-poster_wrapper rounded'> |
||||||
|
<div class="stashr-badge_tl badge rounded-pill bg-info border">[[ issue.issue_number ]]</div> |
||||||
|
<div class="stashr-badge_bl badge rounded-pill bg-dark border"> |
||||||
|
<i class="fas fa-check" v-bind:class="statusFile"></i> |
||||||
|
<i class="fas fa-book" v-bind:class="statusOwned" v-on:click="toggleOwned"></i> |
||||||
|
</div> |
||||||
|
<div class="stashr-badge_br badge rounded-pill bg-dark border"> |
||||||
|
<i class="fas fa-eye" v-bind:class="statusRead" v-on:click="toggleRead"></i> |
||||||
|
</div> |
||||||
|
<div class="stashr-poster_container border rounded"> |
||||||
|
<div class="stashr-overlay_top w-100 center" v-if="hover" > |
||||||
|
<!-- START BUTTONS --> |
||||||
|
<span v-if="issue.issue_file_status"> |
||||||
|
<!-- HAVE --> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') or |
||||||
|
(current_user.role.lower() == 'patron') %} |
||||||
|
<a :href="'/api/downloads/'+issue.issue_id" data-toggle='tooltip' data-placement="top" title="Download"><i class="text-primary px-1 fas fa-download"></i></a> |
||||||
|
{% endif %} |
||||||
|
<a :href="'/read/'+issue.issue_id" data-toggle="tooltip" title="Read"><i class="text-primary px-1 fas fa-book"></i></a> |
||||||
|
<a v-on:click="addToReadingList" data-toggle="tooltip" title="Add to Reading List"><i class="text-primary px-1 fas fa-plus"></i></a> |
||||||
|
<!-- |
||||||
|
<span data-toggle="tooltip" title="Delete"> |
||||||
|
<a data-bs-toggle="modal" data-bs-target="#modalDelete" :data-id="issue.issue_id" v-bind:data-number="issue.issue_number" > |
||||||
|
<i class="text-primary px-1 fas fa-trash-alt"></i> |
||||||
|
</a> |
||||||
|
</span> |
||||||
|
--> |
||||||
|
{{ emit_tep("single_volume_page_top_overlay_have", volume_id=volume_id) }} |
||||||
|
</span> |
||||||
|
<span v-else> |
||||||
|
<!-- MISSING --> |
||||||
|
{% if (current_user.role.lower() == 'admin') or |
||||||
|
(current_user.role.lower() == 'librarian') %} |
||||||
|
<span data-toggle="tooltip" title="Upload to Server"> |
||||||
|
<a data-bs-toggle="modal" data-bs-target="#modalUpload" v-bind:data-issue_id=issue.issue_id v-bind:data-number=issue.issue_number > |
||||||
|
<i class="text-primary px-1 fas fa-cloud-upload-alt"></i> |
||||||
|
</a> |
||||||
|
</span> |
||||||
|
{% endif %} |
||||||
|
{{ emit_tep("single_volume_page_top_overlay_missing", volume_id=volume_id) }} |
||||||
|
</span> |
||||||
|
<!-- END BUTTONS --> |
||||||
|
</div> |
||||||
|
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> |
||||||
|
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalAction" :data-volume_name=issue.volume.volume_name :data-issue_number=issue.issue_number :data-issue_id=issue.issue_id :data-issue_status=issue.issue_file_status v-on:click="this.changeModal"> |
||||||
|
<img class="w-100" loading="lazy" :src="'/images/issues/' + this.issue.issue_id + '.jpg'" @error="$event.target.src=issue.issue_cover_url" /> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
`, |
||||||
|
data() { return { hover: false, have: false, } }, |
||||||
|
computed: { |
||||||
|
statusFile() { |
||||||
|
let classname = 'text-danger'; |
||||||
|
if(this.issue.issue_file_status) { |
||||||
|
classname = 'text-success'; |
||||||
|
}; |
||||||
|
return classname; |
||||||
|
}, |
||||||
|
statusOwned() { |
||||||
|
let classname = 'text-danger'; |
||||||
|
try { |
||||||
|
if(this.issue.owned_status[0].owned_status) { |
||||||
|
classname = 'text-success'; |
||||||
|
} |
||||||
|
} catch { |
||||||
|
classname = 'text-danger'; |
||||||
|
} |
||||||
|
return classname; |
||||||
|
}, |
||||||
|
statusRead() { |
||||||
|
let classname = 'text-danger'; |
||||||
|
try { |
||||||
|
if(this.issue.read_status[0].read_status) { |
||||||
|
classname = 'text-success'; |
||||||
|
} |
||||||
|
} catch { |
||||||
|
classname = 'text-danger'; |
||||||
|
} |
||||||
|
return classname; |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
toggleRead() { |
||||||
|
axios.put('{{ url_for('api.api_put_single_issue', issue_id='ISSUEID') }}'.replace('ISSUEID', this.issue.issue_id), { |
||||||
|
data: { |
||||||
|
read_status: !this.issue.read_status[0].read_status |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Toggled Read Status', 'success') |
||||||
|
this.issue.read_status[0].read_status = !this.issue.read_status[0].read_status; |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
toggleOwned() { |
||||||
|
axios.put('{{ url_for('api.api_put_single_issue', issue_id='ISSUEID') }}'.replace('ISSUEID', this.issue.issue_id), { |
||||||
|
data: { |
||||||
|
owned_status: !this.issue.owned_status[0].owned_status |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Toggled Owned Status', 'success') |
||||||
|
this.issue.owned_status[0].owned_status = !this.issue.owned_status[0].owned_status; |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
addToReadingList() { |
||||||
|
axios.post('{{ url_for('api.api_post_reading_list') }}', { |
||||||
|
issue_id: this.issue.issue_id |
||||||
|
}) |
||||||
|
.then( res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
stashrToast('Added to Reading List', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
imageError() { |
||||||
|
$event.target.src = 'shit' |
||||||
|
}, |
||||||
|
addToCollection() { |
||||||
|
let collection = document.getElementById('collectionName').value |
||||||
|
if (collection == 0) { |
||||||
|
axios.post('{{ url_for('api.api_post_all_collections') }}', { data: { |
||||||
|
collection_name: document.getElementById('newCollection').value, |
||||||
|
issue_id: this.issue.issue_id |
||||||
|
}}) |
||||||
|
.then(function(response) { |
||||||
|
if (response.data.status_code == '200') { |
||||||
|
stashrToast('Added to Collection', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(response.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
} else { |
||||||
|
axios.post('{{ url_for('api.api_post_single_collection', collection_slug='COLLECTIONID') }}'.replace('COLLECTIONID', collection), { |
||||||
|
data: { |
||||||
|
issue_id: this.issue.issue_id |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(function(response){ |
||||||
|
if (response.data.status_code == '200') { |
||||||
|
stashrToast('Added to Collection', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(response.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
} |
||||||
|
}, |
||||||
|
changeModal() { |
||||||
|
app.changeModal(this.issue) |
||||||
|
}, |
||||||
|
toggleFile() { |
||||||
|
this.issue.issue_file_status = 'true'; |
||||||
|
app.$refs.volume.volume.volume_have = parseInt(app.$refs.volume.volume.volume_have) + 1 |
||||||
|
}, |
||||||
|
deleteIssue() { |
||||||
|
axios.delete('{{ url_for('api.api_delete_single_issue', issue_id='ISSUEID') }}'.replace('ISSUEID', this.issue.issue_id)) |
||||||
|
.then( res=> { |
||||||
|
if(res.data.status_code == 200) { |
||||||
|
app.refreshVolumeInfo(); |
||||||
|
this.issue.issue_file_status = 0; |
||||||
|
stashrToast('Issue Deleted', 'success') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
Vue.component('issues', { |
||||||
|
props: ['issues'], |
||||||
|
template: ` |
||||||
|
<div> |
||||||
|
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center"> |
||||||
|
<issue-item |
||||||
|
v-for="issue in issues" |
||||||
|
v-bind:issue="issue" |
||||||
|
v-bind:key="issue.issue_id" |
||||||
|
></issue-item> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
`, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}) |
||||||
|
|
||||||
|
var app = new Vue({ |
||||||
|
el: '#app', |
||||||
|
data: { |
||||||
|
volume: [], |
||||||
|
issues: [], |
||||||
|
issue: [], |
||||||
|
ratings: [], |
||||||
|
collections: [], |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.getVolume(); |
||||||
|
this.getIssues(); |
||||||
|
this.getRatings(); |
||||||
|
this.getCollections(); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getVolume() { |
||||||
|
axios.get('{{ url_for('api.api_get_single_volume', volume_id=volume_id) }}') |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
this.volume = res.data.results |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
getIssues() { |
||||||
|
axios.get('{{ url_for('api.api_get_single_volume_all_issues', volume_id=volume_id) }}', { |
||||||
|
params: { |
||||||
|
offset: this.issues.length |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if(res.data.number_of_page_results > 0) { |
||||||
|
res.data.results.forEach(result => { |
||||||
|
this.issues.push(result) |
||||||
|
}); |
||||||
|
if(this.issues.length < res.data.number_of_total_results) { |
||||||
|
this.getIssues(); |
||||||
|
} |
||||||
|
} |
||||||
|
this.issue = this.issues[0] |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
changeModal(issue) { |
||||||
|
this.issue = issue |
||||||
|
}, |
||||||
|
getRatings() { |
||||||
|
axios.get('{{ url_for('api.api_get_all_ratings') }}') |
||||||
|
.then(res => { |
||||||
|
this.ratings = res.data.results; |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
getCollections() { |
||||||
|
axios.get('{{ url_for('api.api_get_all_collections') }}') |
||||||
|
.then(res => { |
||||||
|
if(res.status == '200') { |
||||||
|
this.collections = res.data.results |
||||||
|
} else { |
||||||
|
stashrToast('Problem Retrieving Collections', 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
refreshVolume() { |
||||||
|
axios.post('{{ url_for('api.api_post_single_volume', volume_id=volume_id) }}') |
||||||
|
.then(res => { |
||||||
|
if(res.data.status_code == '200') { |
||||||
|
this.refreshVolumeInfo(); |
||||||
|
this.refreshIssueInfo(); |
||||||
|
stashrToast('Updated Volume', 'success') |
||||||
|
} else { |
||||||
|
stashrToast(res.data.message, 'error') |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => console.log(err)) |
||||||
|
}, |
||||||
|
refreshCollections() { |
||||||
|
this.collections = []; |
||||||
|
this.getCollections(); |
||||||
|
}, |
||||||
|
refreshVolumeInfo() { |
||||||
|
this.volume = []; |
||||||
|
this.getVolume(); |
||||||
|
}, |
||||||
|
refreshIssueInfo() { |
||||||
|
this.issues = []; |
||||||
|
this.getIssues(); |
||||||
|
}, |
||||||
|
}, |
||||||
|
delimiters: ["[[","]]"] |
||||||
|
}); |
||||||
|
|
||||||
|
{{ emit_tep('single_volume_page_script', volume_id=volume_id) }} |
||||||
|
|
||||||
|
{% endblock %} |