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