diff --git a/boofilsic/settings.py b/boofilsic/settings.py index 086bac28..d5d9f90e 100644 --- a/boofilsic/settings.py +++ b/boofilsic/settings.py @@ -13,7 +13,6 @@ https://docs.djangoproject.com/en/3.0/ref/settings/ import os import psycopg2.extensions - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -29,6 +28,11 @@ DEBUG = True ALLOWED_HOSTS = ['*'] +# To allow debug in template context +# https://docs.djangoproject.com/en/3.1/ref/settings/#internal-ips +INTERNAL_IPS = [ + "127.0.0.1" +] # Application definition @@ -39,10 +43,18 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.humanize', + 'django.contrib.postgres', 'markdownx', - 'users.apps.UsersConfig', + 'management.apps.ManagementConfig', + 'mastodon.apps.MastodonConfig', 'common.apps.CommonConfig', + 'users.apps.UsersConfig', 'books.apps.BooksConfig', + 'movies.apps.MoviesConfig', + 'music.apps.MusicConfig', + 'games.apps.GamesConfig', + 'easy_thumbnails', ] MIDDLEWARE = [ @@ -86,7 +98,7 @@ if DEBUG: 'NAME': 'test', 'USER': 'donotban', 'PASSWORD': 'donotbansilvousplait', - 'HOST': '172.18.47.7', + 'HOST': '172.18.116.29', 'OPTIONS': { 'client_encoding': 'UTF8', # 'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_DEFAULT, @@ -112,7 +124,7 @@ else: # https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#authentication-backends AUTHENTICATION_BACKENDS = [ - 'common.mastodon.auth.OAuth2Backend', + 'mastodon.auth.OAuth2Backend', ] @@ -137,31 +149,30 @@ if not DEBUG: SECURE_HSTS_PRELOAD = True SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_SECONDS = 31536000 + LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'simple': { - 'format': '{levelname} {asctime} {module} {message}', - 'style': '{', + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'simple': { + 'format': '{levelname} {asctime} {name}:{lineno} {message}', + 'style': '{', + }, + }, + 'handlers': { + 'file': { + 'level': 'INFO', + 'class': 'logging.FileHandler', + 'filename': os.path.join(BASE_DIR, 'log'), + 'formatter': 'simple' + }, }, - }, - 'handlers': { - 'file': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'filename': os.path.join(BASE_DIR, 'log'), - 'formatter': 'simple' - }, - }, - 'loggers': { - 'django': { + 'root': { 'handlers': ['file'], 'level': 'INFO', 'propagate': True, }, - }, -} + } # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ @@ -169,22 +180,31 @@ if not DEBUG: STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'static/') +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' + AUTH_USER_MODEL = 'users.User' MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') -CLIENT_ID = 'kEbwT9Je5HHg4FoLx4nb0tNaIrPNs5Mw6AYlQlsj2_4' -CLIENT_SECRET = 'xwmEvlmudLCkBmvdzGf8m41Ug5o5di9xnDqeVLrcKSg' +# Mastodon configs +CLIENT_NAME = 'NiceDB' +APP_WEBSITE = 'https://nicedb.org' +REDIRECT_URIS = "https://nicedb.org/users/OAuth2_login/\nhttps://www.nicedb.org/users/OAuth2_login/" -# Path to save report related images, ends without slash +# Path to save report related images, ends with slash REPORT_MEDIA_PATH_ROOT = 'report/' MARKDOWNX_MEDIA_PATH = 'review/' BOOK_MEDIA_PATH_ROOT = 'book/' DEFAULT_BOOK_IMAGE = os.path.join(BOOK_MEDIA_PATH_ROOT, 'default.svg') - -# Mastodon domain name -MASTODON_DOMAIN_NAME = 'donotban.com' +MOVIE_MEDIA_PATH_ROOT = 'movie/' +DEFAULT_MOVIE_IMAGE = os.path.join(MOVIE_MEDIA_PATH_ROOT, 'default.svg') +SONG_MEDIA_PATH_ROOT = 'song/' +DEFAULT_SONG_IMAGE = os.path.join(SONG_MEDIA_PATH_ROOT, 'default.svg') +ALBUM_MEDIA_PATH_ROOT = 'album/' +DEFAULT_ALBUM_IMAGE = os.path.join(ALBUM_MEDIA_PATH_ROOT, 'default.svg') +GAME_MEDIA_PATH_ROOT = 'game/' +DEFAULT_GAME_IMAGE = os.path.join(GAME_MEDIA_PATH_ROOT, 'default.svg') # Timeout of requests to Mastodon, in seconds MASTODON_TIMEOUT = 30 @@ -208,8 +228,31 @@ LOGIN_URL = '/users/login/' ADMIN_URL = 'lpLuTqX72Bt2hLfxxRYKeTZdE59Y2hLfpLuTqX72Btx9sXuljYK4tYEmjrHd' # Luminati proxy settings -LUMINATI_USERNAME = '***REMOVED***' -LUMINATI_PASSWORD = '***REMOVED***' +LUMINATI_USERNAME = 'lum-customer-hl_7bed6f85-zone-static' +LUMINATI_PASSWORD = 'dwy4lz5ck438' + +# Spotify credentials +# SPOTIFY_CLIENT_ID = "***REMOVED***" +# SPOTIFY_CLIENT_SECRET = "***REMOVED***" +SPOTIFY_CREDENTIAL = "***REMOVED***" + +# IMDb API service https://imdb-api.com/ +IMDB_API_KEY = "***REMOVED***" + +# Thumbnail setting +# It is possible to optimize the image size even more: https://easy-thumbnails.readthedocs.io/en/latest/ref/optimize/ +THUMBNAIL_ALIASES = { + '': { + 'normal': { + 'size': (200, 200), + 'crop': 'scale', + 'autocrop': True, + }, + }, +} +# THUMBNAIL_PRESERVE_EXTENSIONS = ('svg',) +if DEBUG: + THUMBNAIL_DEBUG = True # https://django-debug-toolbar.readthedocs.io/en/latest/ # maybe benchmarking before deployment diff --git a/boofilsic/urls.py b/boofilsic/urls.py index 1c9cec13..213c393e 100644 --- a/boofilsic/urls.py +++ b/boofilsic/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ path('books/', include('books.urls')), path('movies/', include('movies.urls')), path('music/', include('music.urls')), + path('games/', include('games.urls')), path('announcement/', include('management.urls')), path('', include('common.urls')), diff --git a/books/templates/books/detail.html b/books/templates/books/detail.html index 885b27dd..fd8ee1e7 100644 --- a/books/templates/books/detail.html +++ b/books/templates/books/detail.html @@ -89,7 +89,7 @@ {% if book.other_info %} {% for k, v in book.other_info.items %}
- {{k}}:{{v}} + {{ k }}:{{ v | urlize }}
{% endfor %} {% endif %} diff --git a/books/templates/books/scrape.html b/books/templates/books/scrape.html index d24537a9..6ad5a8a1 100644 --- a/books/templates/books/scrape.html +++ b/books/templates/books/scrape.html @@ -44,7 +44,7 @@
{% trans '根据豆瓣内容填写下方表单' %}
- +
diff --git a/common/config.py b/common/config.py new file mode 100644 index 00000000..2f5b69df --- /dev/null +++ b/common/config.py @@ -0,0 +1,20 @@ +# how many items are showed in one search result page +ITEMS_PER_PAGE = 20 + +# how many pages links in the pagination +PAGE_LINK_NUMBER = 7 + +# max tags on list page +TAG_NUMBER_ON_LIST = 5 + +# how many books have in each set at the home page +BOOKS_PER_SET = 5 + +# how many movies have in each set at the home page +MOVIES_PER_SET = 5 + +# how many music items have in each set at the home page +MUSIC_PER_SET = 5 + +# how many games have in each set at the home page +GAMES_PER_SET = 5 diff --git a/common/models.py b/common/models.py index cfe802a9..745a7c4d 100644 --- a/common/models.py +++ b/common/models.py @@ -22,6 +22,7 @@ class SourceSiteEnum(models.TextChoices): DOUBAN = "douban", _("豆瓣") SPOTIFY = "spotify", _("Spotify") IMDB = "imdb", _("IMDb") + STEAM = "steam", _("STEAM") class Entity(models.Model): diff --git a/common/scraper.py b/common/scraper.py index 2c1f323e..37ad1d58 100644 --- a/common/scraper.py +++ b/common/scraper.py @@ -22,6 +22,8 @@ from books.models import Book from books.forms import BookForm from music.models import Album, Song from music.forms import AlbumForm, SongForm +from games.models import Game +from games.forms import GameForm RE_NUMBERS = re.compile(r"\d+\d*") @@ -1018,3 +1020,131 @@ class ImdbMovieScraper(AbstractScraper): @classmethod def get_api_url(cls, url): return f"https://imdb-api.com/zh/API/Title/{IMDB_API_KEY}/{cls.regex.findall(url)[0]}/FullActor," + + +class DoubanGameScraper(AbstractScraper): + site_name = SourceSiteEnum.DOUBAN.value + host = 'www.douban.com/game/' + data_class = Game + form_class = GameForm + + regex = re.compile(r"https://www\.douban\.com/game/\d+/{0,1}") + + def scrape(self, url): + headers = DEFAULT_REQUEST_HEADERS.copy() + headers['Host'] = 'www.douban.com' + content = self.download_page(url, headers) + + try: + raw_title = content.xpath( + "//div[@id='content']/h1/text()")[0].strip() + except IndexError: + raise ValueError("given url contains no movie info") + + title = raw_title + + other_title_elem = content.xpath( + "//dl[@class='game-attr']//dt[text()='别名:']/following-sibling::dd[1]/text()") + other_title = other_title_elem[0].strip().split(' / ') if other_title_elem else None + + developer_elem = content.xpath( + "//dl[@class='game-attr']//dt[text()='开发商:']/following-sibling::dd[1]/text()") + developer = developer_elem[0].strip().split(' / ') if developer_elem else None + + publisher_elem = content.xpath( + "//dl[@class='game-attr']//dt[text()='发行商:']/following-sibling::dd[1]/text()") + publisher = publisher_elem[0].strip().split(' / ') if publisher_elem else None + + platform_elem = content.xpath( + "//dl[@class='game-attr']//dt[text()='平台:']/following-sibling::dd[1]/a/text()") + platform = platform_elem if platform_elem else None + + genre_elem = content.xpath( + "//dl[@class='game-attr']//dt[text()='类型:']/following-sibling::dd[1]/a/text()") + genre = None + if genre_elem: + genre = [g for g in genre_elem if g != '游戏'] + + date_elem = content.xpath( + "//dl[@class='game-attr']//dt[text()='发行日期:']/following-sibling::dd[1]/text()") + release_date = dateparser.parse(date_elem[0].strip(), settings={ + "RELATIVE_BASE": datetime.datetime(1900, 1, 1)}) if date_elem else None + + brief_elem = content.xpath("//div[@class='mod item-desc']/p/text()") + brief = '\n'.join(brief_elem) if brief_elem else None + + img_url_elem = content.xpath( + "//div[@class='item-subject-info']/div[@class='pic']//img/@src") + img_url = img_url_elem[0].strip() if img_url_elem else None + raw_img, ext = self.download_image(img_url) + + data = { + 'title': title, + 'other_title': other_title, + 'developer': developer, + 'publisher': publisher, + 'release_date': release_date, + 'genre': genre, + 'platform': platform, + 'brief': brief, + 'other_info': None, + 'source_site': self.site_name, + 'source_url': self.get_effective_url(url), + } + + self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext + return data, raw_img + + +class SteamGameScraper(AbstractScraper): + site_name = SourceSiteEnum.STEAM.value + host = 'store.steampowered.com' + data_class = Game + form_class = GameForm + + regex = re.compile(r"https://store\.steampowered\.com/app/\d+/{0,1}") + + def scrape(self, url): + headers = DEFAULT_REQUEST_HEADERS.copy() + headers['Host'] = self.host + content = self.download_page(url, headers) + + title = content.xpath("//div[@class='apphub_AppName']/text()")[0] + developer = content.xpath("//div[@id='developers_list']/a/text()") + publisher = content.xpath("//div[@class='glance_ctn']//div[@class='dev_row'][2]//a/text()") + release_date = dateparser.parse( + content.xpath( + "//div[@class='release_date']/div[@class='date']/text()")[0], + settings={ + "RELATIVE_BASE": datetime.datetime(1900, 1, 1) + } + ) + + genre = content.xpath( + "//div[@class='details_block']/b[2]/following-sibling::a/text()") + + platform = ['PC'] + + brief = content.xpath( + "//div[@class='game_description_snippet']/text()")[0].strip() + + img_url = content.xpath("//img[@class='game_header_image_full']/@src")[ + 0].replace("header.jpg", "library_600x900.jpg") + raw_img, ext = self.download_image(img_url) + + data = { + 'title': title, + 'other_title': None, + 'developer': developer, + 'publisher': publisher, + 'release_date': release_date, + 'genre': genre, + 'platform': platform, + 'brief': brief, + 'other_info': None, + 'source_site': self.site_name, + 'source_url': self.get_effective_url(url), + } + + self.raw_data, self.raw_img, self.img_ext = data, raw_img, ext + return data, raw_img diff --git a/common/static/css/boofilsic.css b/common/static/css/boofilsic.css index 8623d363..0ddbdc36 100644 --- a/common/static/css/boofilsic.css +++ b/common/static/css/boofilsic.css @@ -1246,6 +1246,14 @@ select::placeholder { font-weight: bold; } +.source-label.source-label__steam { + background: linear-gradient(30deg, #1387b8, #111d2e); + color: white; + border: none; + font-weight: 600; + padding-top: 2px; +} + .main-section-wrapper { padding: 32px 48px 32px 36px; background-color: #f7f7f7; diff --git a/common/static/css/boofilsic.min.css b/common/static/css/boofilsic.min.css index 53607bef..83a180ca 100644 --- a/common/static/css/boofilsic.min.css +++ b/common/static/css/boofilsic.min.css @@ -1 +1 @@ -@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;-o-object-fit:contain;object-fit:contain}img.emoji{height:14px;-webkit-box-sizing:border-box;box-sizing:border-box;-o-object-fit:contain;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{-webkit-box-sizing:inherit;box-sizing:inherit}html{-webkit-box-sizing:border-box;box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='date'],input[type='time'],input[type='color'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='date']:focus,input[type='time']:focus,input[type='color']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::-webkit-input-placeholder,input[type='number']::-webkit-input-placeholder,input[type='password']::-webkit-input-placeholder,input[type='search']::-webkit-input-placeholder,input[type='tel']::-webkit-input-placeholder,input[type='text']::-webkit-input-placeholder,input[type='url']::-webkit-input-placeholder,input[type='date']::-webkit-input-placeholder,input[type='time']::-webkit-input-placeholder,input[type='color']::-webkit-input-placeholder,textarea::-webkit-input-placeholder,select::-webkit-input-placeholder{color:#ccc}input[type='email']:-ms-input-placeholder,input[type='number']:-ms-input-placeholder,input[type='password']:-ms-input-placeholder,input[type='search']:-ms-input-placeholder,input[type='tel']:-ms-input-placeholder,input[type='text']:-ms-input-placeholder,input[type='url']:-ms-input-placeholder,input[type='date']:-ms-input-placeholder,input[type='time']:-ms-input-placeholder,input[type='color']:-ms-input-placeholder,textarea:-ms-input-placeholder,select:-ms-input-placeholder{color:#ccc}input[type='email']::-ms-input-placeholder,input[type='number']::-ms-input-placeholder,input[type='password']::-ms-input-placeholder,input[type='search']::-ms-input-placeholder,input[type='tel']::-ms-input-placeholder,input[type='text']::-ms-input-placeholder,input[type='url']::-ms-input-placeholder,input[type='date']::-ms-input-placeholder,input[type='time']::-ms-input-placeholder,input[type='color']::-ms-input-placeholder,textarea::-ms-input-placeholder,select::-ms-input-placeholder{color:#ccc}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,input[type='date']::placeholder,input[type='time']::placeholder,input[type='color']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::-moz-selection{color:white;background-color:#00a1cc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;-webkit-box-sizing:border-box;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative}.navbar .navbar__logo{-ms-flex-preferred-size:100px;flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-flex:1;-ms-flex:1;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;-webkit-appearance:auto;-moz-appearance:auto;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;-webkit-transform:scale(0.7);transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:distribute;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.grid .grid__aside--tablet-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.grid--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__main--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__aside--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-edit svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-save svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;-webkit-transform:translateX(-50%) scale(0.4);transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{-webkit-transform-origin:40px 40px;transform-origin:40px 40px;-webkit-animation:spinner 1.2s linear infinite;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){-webkit-transform:rotate(0deg);transform:rotate(0deg);-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner div:nth-child(2){-webkit-transform:rotate(30deg);transform:rotate(30deg);-webkit-animation-delay:-1s;animation-delay:-1s}.spinner div:nth-child(3){-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner div:nth-child(4){-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-animation-delay:-.8s;animation-delay:-.8s}.spinner div:nth-child(5){-webkit-transform:rotate(120deg);transform:rotate(120deg);-webkit-animation-delay:-.7s;animation-delay:-.7s}.spinner div:nth-child(6){-webkit-transform:rotate(150deg);transform:rotate(150deg);-webkit-animation-delay:-.6s;animation-delay:-.6s}.spinner div:nth-child(7){-webkit-transform:rotate(180deg);transform:rotate(180deg);-webkit-animation-delay:-.5s;animation-delay:-.5s}.spinner div:nth-child(8){-webkit-transform:rotate(210deg);transform:rotate(210deg);-webkit-animation-delay:-.4s;animation-delay:-.4s}.spinner div:nth-child(9){-webkit-transform:rotate(240deg);transform:rotate(240deg);-webkit-animation-delay:-.3s;animation-delay:-.3s}.spinner div:nth-child(10){-webkit-transform:rotate(270deg);transform:rotate(270deg);-webkit-animation-delay:-.2s;animation-delay:-.2s}.spinner div:nth-child(11){-webkit-transform:rotate(300deg);transform:rotate(300deg);-webkit-animation-delay:-.1s;animation-delay:-.1s}.spinner div:nth-child(12){-webkit-transform:rotate(330deg);transform:rotate(330deg);-webkit-animation-delay:0s;animation-delay:0s}@-webkit-keyframes spinner{0%{opacity:1}100%{opacity:0}}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;-webkit-filter:opacity(20%);filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;word-break:keep-all;opacity:1;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border:none;color:#fff;background-color:#319840}.source-label.source-label__spotify{background-color:#1ed760;color:#000;border:none;font-weight:bold}.source-label.source-label__imdb{background-color:#F5C518;color:#121212;border:none;font-weight:bold}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{-o-object-fit:contain;object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{max-width:100%}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;float:left;-o-object-fit:contain;object-fit:contain;max-width:150px;-o-object-position:top;object-position:top}.entity-detail .entity-detail__img-origin{cursor:-webkit-zoom-in;cursor:zoom-in}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:202px}.entity-desc .entity-desc__unfold-button{display:-webkit-box;display:-ms-flexbox;display:flex;color:#00a1cc;background-color:transparent;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{position:relative;margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:5px}.entity-sort .entity-sort__entity-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-ms-flex-wrap:wrap;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;-ms-flex-preferred-size:20%;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.entity-sort--placeholder{border:dashed #bbb 4px}.entity-sort--hover{padding:10px;border:dashed #00a1cc 2px !important;border-radius:3px}.entity-sort--sortable{padding:10px;margin:10px 0;border:dashed #bbb 2px;cursor:all-scroll}.entity-sort--hidden{opacity:0.4}.entity-sort-control{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.entity-sort-control__button{margin-top:5px;margin-left:12px;padding:0 2px;cursor:pointer;color:#bbb}.entity-sort-control__button:hover{color:#00a1cc}.entity-sort-control__button:hover>.icon-save svg,.entity-sort-control__button:hover>.icon-edit svg{fill:#00a1cc}.entity-sort-control__button--float-right{position:absolute;top:4px;right:10px}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}.track-carousel{position:relative;margin-top:5px}.track-carousel__content{overflow:auto;scroll-behavior:smooth;scrollbar-width:none;display:-webkit-box;display:-ms-flexbox;display:flex;margin:auto;-webkit-box-sizing:border-box;box-sizing:border-box;padding-bottom:10px}.track-carousel__content::-webkit-scrollbar{height:3px;width:1px;background-color:#e5e5e5}.track-carousel__content::-webkit-scrollbar-thumb{background-color:#bbb}.track-carousel__track{text-align:center;overflow:hidden;text-overflow:ellipsis;min-width:18%;max-width:18%;margin-right:2.5%}.track-carousel__track img{-o-object-fit:contain;object-fit:contain}.track-carousel__track-title{white-space:nowrap}.track-carousel__button{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-line-pack:center;align-content:center;background:white;border:none;padding:8px;border-radius:50%;outline:0;cursor:pointer;position:absolute;top:50%}.track-carousel__button--prev{left:0;-webkit-transform:translate(50%, -50%);transform:translate(50%, -50%)}.track-carousel__button--next{right:0;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}@media (max-width: 575.98px){.entity-list .entity-list__entity{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{-ms-flex-preferred-size:50%;flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}.track-carousel__content{padding-bottom:10px}.track-carousel__track{min-width:31%;max-width:31%;margin-right:4.5%}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:-webkit-box;display:-ms-flexbox;display:flex}}.aside-section-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.action-panel .action-panel__button-group--center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.user-relation .user-relation__related-user{-ms-flex-preferred-size:25%;flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{width:48px}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-card--horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{-ms-flex-preferred-size:100px;flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card--horizontal{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.action-panel .action-panel__button-group{-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer}.relation-dropdown .relation-dropdown__button:focus{background-color:red}.relation-dropdown .relation-dropdown__button>.icon-arrow{-webkit-transition:-webkit-transform 0.3s;transition:-webkit-transform 0.3s;transition:transform 0.3s;transition:transform 0.3s, -webkit-transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow>svg{fill:#00a1cc}.relation-dropdown .relation-dropdown__button>.icon-arrow--expand{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button+.relation-dropdown__body--expand{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.entity-card{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset} +@import url(https://cdn.jsdelivr.net/npm/skeleton-css@2.0.4/css/normalize.css);.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#00a1cc;border:0.1rem solid #00a1cc;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.4rem;letter-spacing:.1rem;line-height:3.4rem;padding:0 2.8rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#00a1cc;border-color:#00a1cc}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#00a1cc}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#00a1cc}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#00a1cc}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#00a1cc}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem;width:100%}select{width:100%}label,legend{display:block;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:1rem}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%;-o-object-fit:contain;object-fit:contain}img.emoji{height:14px;-webkit-box-sizing:border-box;box-sizing:border-box;-o-object-fit:contain;object-fit:contain;position:relative;top:3px}img.emoji--large{height:20px;position:relative;top:2px}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}.highlight{font-weight:bold}:root{font-size:10px}*,*:after,*:before{-webkit-box-sizing:inherit;box-sizing:inherit}html{-webkit-box-sizing:border-box;box-sizing:border-box;height:100%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif;font-size:1.3rem;font-weight:300;letter-spacing:.05rem;line-height:1.6;margin:0;height:100%}textarea{font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Microsoft YaHei Light', sans-serif}a{color:#00a1cc;text-decoration:none}a:active,a:hover,a:hover:visited{color:#606c76}li{list-style:none}input[type=text]::-ms-clear,input[type=text]::-ms-reveal{display:none;width:0;height:0}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-results-button,input[type="search"]::-webkit-search-results-decoration{display:none}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='date'],input[type='time'],input[type='color'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='date']:focus,input[type='time']:focus,input[type='color']:focus,textarea:focus,select:focus{border-color:#00a1cc;outline:0}input[type='email']::-webkit-input-placeholder,input[type='number']::-webkit-input-placeholder,input[type='password']::-webkit-input-placeholder,input[type='search']::-webkit-input-placeholder,input[type='tel']::-webkit-input-placeholder,input[type='text']::-webkit-input-placeholder,input[type='url']::-webkit-input-placeholder,input[type='date']::-webkit-input-placeholder,input[type='time']::-webkit-input-placeholder,input[type='color']::-webkit-input-placeholder,textarea::-webkit-input-placeholder,select::-webkit-input-placeholder{color:#ccc}input[type='email']:-ms-input-placeholder,input[type='number']:-ms-input-placeholder,input[type='password']:-ms-input-placeholder,input[type='search']:-ms-input-placeholder,input[type='tel']:-ms-input-placeholder,input[type='text']:-ms-input-placeholder,input[type='url']:-ms-input-placeholder,input[type='date']:-ms-input-placeholder,input[type='time']:-ms-input-placeholder,input[type='color']:-ms-input-placeholder,textarea:-ms-input-placeholder,select:-ms-input-placeholder{color:#ccc}input[type='email']::-ms-input-placeholder,input[type='number']::-ms-input-placeholder,input[type='password']::-ms-input-placeholder,input[type='search']::-ms-input-placeholder,input[type='tel']::-ms-input-placeholder,input[type='text']::-ms-input-placeholder,input[type='url']::-ms-input-placeholder,input[type='date']::-ms-input-placeholder,input[type='time']::-ms-input-placeholder,input[type='color']::-ms-input-placeholder,textarea::-ms-input-placeholder,select::-ms-input-placeholder{color:#ccc}input[type='email']::placeholder,input[type='number']::placeholder,input[type='password']::placeholder,input[type='search']::placeholder,input[type='tel']::placeholder,input[type='text']::placeholder,input[type='url']::placeholder,input[type='date']::placeholder,input[type='time']::placeholder,input[type='color']::placeholder,textarea::placeholder,select::placeholder{color:#ccc}::-moz-selection{color:white;background-color:#00a1cc}::selection{color:white;background-color:#00a1cc}.navbar{background-color:#f7f7f7;-webkit-box-sizing:border-box;box-sizing:border-box;padding:10px 0;margin-bottom:50px;border-bottom:#ccc 0.5px solid}.navbar .navbar__wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative}.navbar .navbar__logo{-ms-flex-preferred-size:100px;flex-basis:100px}.navbar .navbar__logo-link{display:inline-block}.navbar .navbar__link-list{margin:0;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.navbar .navbar__link{margin:9px;color:#606c76}.navbar .navbar__link:active,.navbar .navbar__link:hover,.navbar .navbar__link:hover:visited{color:#00a1cc}.navbar .navbar__link:visited{color:#606c76}.navbar .navbar__search-box{margin:0 12% 0 15px;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-flex:1;-ms-flex:1;flex:1}.navbar .navbar__search-box>input[type="search"]{border-top-right-radius:0;border-bottom-right-radius:0;margin:0;height:32px;background-color:white !important;width:100%}.navbar .navbar__search-box .navbar__search-dropdown{margin:0;margin-left:-1px;padding:0;padding-left:10px;color:#606c76;-webkit-appearance:auto;-moz-appearance:auto;appearance:auto;background-color:white;height:32px;width:80px;border-top-left-radius:0;border-bottom-left-radius:0}.navbar .navbar__dropdown-btn{display:none;padding:0;margin:0;border:none;background-color:transparent;color:#00a1cc}.navbar .navbar__dropdown-btn:focus,.navbar .navbar__dropdown-btn:hover{background-color:transparent;color:#606c76}@media (max-width: 575.98px){.navbar{padding:2px 0}.navbar .navbar__wrapper{display:block}.navbar .navbar__logo-img{width:72px;margin-right:10px;position:relative;top:7px}.navbar .navbar__link-list{margin-top:7px;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.navbar .navbar__dropdown-btn{display:block;position:absolute;right:5px;top:3px;-webkit-transform:scale(0.7);transform:scale(0.7)}.navbar .navbar__dropdown-btn:hover+.navbar__link-list{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.navbar .navbar__search-box{margin:0;width:46vw}.navbar .navbar__search-box>input[type="search"]{height:26px;padding:4px 6px;width:32vw}.navbar .navbar__search-box .navbar__search-dropdown{cursor:pointer;height:26px;width:80px;padding-left:5px}}@media (max-width: 991.98px){.navbar{margin-bottom:20px}}.grid{margin:0 auto;position:relative;max-width:110rem;padding:0 2.0rem;width:100%}.grid .grid__main{width:70%;float:left;position:relative}.grid .grid__aside{width:26%;float:right;position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:distribute;justify-content:space-around}.grid::after{content:' ';clear:both;display:table}@media (max-width: 575.98px){.grid .grid__aside{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}}@media (max-width: 991.98px){.grid .grid__main{width:100%;float:none}.grid .grid__aside{width:100%;float:none;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.grid .grid__aside--tablet-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.grid--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__main--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.grid .grid__aside--reverse-order{-webkit-transform:scaleY(-1);transform:scaleY(-1)}}.pagination{text-align:center;width:100%}.pagination .pagination__page-link{font-weight:normal;margin:0 5px}.pagination .pagination__page-link--current{font-weight:bold;font-size:1.2em;color:#606c76}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:18px}.pagination .pagination__nav-link--left-margin{margin-left:18px}.pagination .pagination__nav-link--hidden{display:none}@media (max-width: 575.98px){.pagination .pagination__page-link{margin:0 3px}.pagination .pagination__nav-link{font-size:1.4em;margin:0 2px}.pagination .pagination__nav-link--right-margin{margin-right:10px}.pagination .pagination__nav-link--left-margin{margin-left:10px}}#page-wrapper{position:relative;min-height:100vh;z-index:0}#content-wrapper{padding-bottom:160px}.footer{padding-top:0.4em !important;text-align:center;margin-bottom:4px !important;position:absolute !important;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);bottom:0;width:100%}.footer__border{padding-top:4px;border-top:#f7f7f7 solid 2px}.footer__link{margin:0 12px;white-space:nowrap}@media (max-width: 575.98px){#content-wrapper{padding-bottom:120px}}.icon-lock svg{fill:#ccc;height:12px;position:relative;top:1px;margin-left:3px}.icon-edit svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-save svg{fill:#ccc;height:12px;position:relative;top:2px}.icon-cross svg{fill:#ccc;height:10px;position:relative}.icon-arrow svg{fill:#606c76;height:15px;position:relative;top:3px}.spinner{display:inline-block;position:relative;left:50%;-webkit-transform:translateX(-50%) scale(0.4);transform:translateX(-50%) scale(0.4);width:80px;height:80px}.spinner div{-webkit-transform-origin:40px 40px;transform-origin:40px 40px;-webkit-animation:spinner 1.2s linear infinite;animation:spinner 1.2s linear infinite}.spinner div::after{content:" ";display:block;position:absolute;top:3px;left:37px;width:6px;height:18px;border-radius:20%;background:#606c76}.spinner div:nth-child(1){-webkit-transform:rotate(0deg);transform:rotate(0deg);-webkit-animation-delay:-1.1s;animation-delay:-1.1s}.spinner div:nth-child(2){-webkit-transform:rotate(30deg);transform:rotate(30deg);-webkit-animation-delay:-1s;animation-delay:-1s}.spinner div:nth-child(3){-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-delay:-.9s;animation-delay:-.9s}.spinner div:nth-child(4){-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-animation-delay:-.8s;animation-delay:-.8s}.spinner div:nth-child(5){-webkit-transform:rotate(120deg);transform:rotate(120deg);-webkit-animation-delay:-.7s;animation-delay:-.7s}.spinner div:nth-child(6){-webkit-transform:rotate(150deg);transform:rotate(150deg);-webkit-animation-delay:-.6s;animation-delay:-.6s}.spinner div:nth-child(7){-webkit-transform:rotate(180deg);transform:rotate(180deg);-webkit-animation-delay:-.5s;animation-delay:-.5s}.spinner div:nth-child(8){-webkit-transform:rotate(210deg);transform:rotate(210deg);-webkit-animation-delay:-.4s;animation-delay:-.4s}.spinner div:nth-child(9){-webkit-transform:rotate(240deg);transform:rotate(240deg);-webkit-animation-delay:-.3s;animation-delay:-.3s}.spinner div:nth-child(10){-webkit-transform:rotate(270deg);transform:rotate(270deg);-webkit-animation-delay:-.2s;animation-delay:-.2s}.spinner div:nth-child(11){-webkit-transform:rotate(300deg);transform:rotate(300deg);-webkit-animation-delay:-.1s;animation-delay:-.1s}.spinner div:nth-child(12){-webkit-transform:rotate(330deg);transform:rotate(330deg);-webkit-animation-delay:0s;animation-delay:0s}@-webkit-keyframes spinner{0%{opacity:1}100%{opacity:0}}@keyframes spinner{0%{opacity:1}100%{opacity:0}}.bg-mask{background-color:black;z-index:1;-webkit-filter:opacity(20%);filter:opacity(20%);position:fixed;width:100%;height:100%;left:0;top:0;display:none}.mark-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.mark-modal .mark-modal__head{margin-bottom:20px}.mark-modal .mark-modal__head::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__title{font-weight:bold;font-size:1.2em;float:left}.mark-modal .mark-modal__close-button{float:right;cursor:pointer}.mark-modal .mark-modal__confirm-button{float:right}.mark-modal input[type="radio"]{margin-right:0}.mark-modal .mark-modal__rating-star{display:inline;float:left;position:relative;left:-3px}.mark-modal .mark-modal__status-radio{float:right}.mark-modal .mark-modal__status-radio ul{margin-bottom:0}.mark-modal .mark-modal__status-radio li,.mark-modal .mark-modal__status-radio label{display:inline}.mark-modal .mark-modal__status-radio input[type="radio"]{position:relative;top:1px}.mark-modal .mark-modal__clear{content:' ';clear:both;display:table}.mark-modal .mark-modal__content-input,.mark-modal form textarea{height:200px;width:100%;margin-top:5px;margin-bottom:5px;resize:vertical}.mark-modal .mark-modal__tag{margin-bottom:20px}.mark-modal .mark-modal__option{margin-bottom:24px}.mark-modal .mark-modal__option::after{content:' ';clear:both;display:table}.mark-modal .mark-modal__visibility-radio{float:left}.mark-modal .mark-modal__visibility-radio ul,.mark-modal .mark-modal__visibility-radio li,.mark-modal .mark-modal__visibility-radio label{display:inline}.mark-modal .mark-modal__visibility-radio label{font-size:normal}.mark-modal .mark-modal__visibility-radio input[type="radio"]{position:relative;top:2px}.mark-modal .mark-modal__share-checkbox{float:right}.mark-modal .mark-modal__share-checkbox input[type="checkbox"]{position:relative;top:2px}.confirm-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.confirm-modal .confirm-modal__head{margin-bottom:20px}.confirm-modal .confirm-modal__head::after{content:' ';clear:both;display:table}.confirm-modal .confirm-modal__title{font-weight:bold;font-size:1.2em;float:left}.confirm-modal .confirm-modal__close-button{float:right;cursor:pointer}.confirm-modal .confirm-modal__confirm-button{float:right}.announcement-modal{z-index:2;display:none;position:fixed;width:500px;top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);background-color:#f7f7f7;padding:20px 20px 10px 20px;color:#606c76}.announcement-modal .announcement-modal__head{margin-bottom:20px}.announcement-modal .announcement-modal__head::after{content:' ';clear:both;display:table}.announcement-modal .announcement-modal__title{font-weight:bold;font-size:1.2em;float:left}.announcement-modal .announcement-modal__close-button{float:right;cursor:pointer}.announcement-modal .announcement-modal__confirm-button{float:right}.announcement-modal .announcement-modal__body{overflow-y:auto;max-height:64vh}.announcement-modal .announcement-modal__body .announcement__title{display:inline-block}.announcement-modal .announcement-modal__body .announcement__datetime{color:#ccc;margin-left:10px}.announcement-modal .announcement-modal__body .announcement__content{word-break:break-all}@media (max-width: 575.98px){.mark-modal,.confirm-modal,.announcement-modal{width:100%}}.source-label{display:inline;background:transparent;border-radius:.3rem;border-style:solid;border-width:.1rem;line-height:1.2rem;font-size:1.1rem;margin:3px;padding:1px 3px;padding-top:2px;font-weight:lighter;letter-spacing:0.1rem;word-break:keep-all;opacity:1;position:relative;top:-1px}.source-label.source-label__in-site{border-color:#00a1cc;color:#00a1cc}.source-label.source-label__douban{border:none;color:#fff;background-color:#319840}.source-label.source-label__spotify{background-color:#1ed760;color:#000;border:none;font-weight:bold}.source-label.source-label__imdb{background-color:#F5C518;color:#121212;border:none;font-weight:bold}.source-label.source-label__steam{background:linear-gradient(30deg, #1387b8, #111d2e);color:white;border:none;font-weight:600;padding-top:2px}.main-section-wrapper{padding:32px 48px 32px 36px;background-color:#f7f7f7;overflow:auto}.main-section-wrapper input,.main-section-wrapper select{width:100%}.entity-list .entity-list__title{margin-bottom:20px}.entity-list .entity-list__entity{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:36px}.entity-list .entity-list__entity::after{content:' ';clear:both;display:table}.entity-list .entity-list__entity-img{-o-object-fit:contain;object-fit:contain;min-width:130px;max-width:130px}.entity-list .entity-list__entity-text{margin-left:20px;overflow:hidden;width:100%}.entity-list .entity-list__entity-text .tag-collection{margin-left:-3px}.entity-list .entity-list__entity-link{font-size:1.2em}.entity-list .entity-list__entity-title{display:block}.entity-list .entity-list__entity-category{color:#bbb;margin-left:5px;position:relative;top:-1px}.entity-list .entity-list__entity-info{max-width:73%;white-space:nowrap;overflow:hidden;display:inline-block;text-overflow:ellipsis;position:relative;top:0.52em}.entity-list .entity-list__entity-info--full-length{max-width:100%}.entity-list .entity-list__entity-brief{margin-top:8px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;margin-bottom:0}.entity-list .entity-list__rating{display:inline-block;margin:0}.entity-list .entity-list__rating--empty{margin-right:5px}.entity-list .entity-list__rating-score{margin-right:5px;position:relative;top:1px}.entity-list .entity-list__rating-star{display:inline;position:relative;top:0.3em;left:-0.3em}.entity-detail .entity-detail__img{height:210px;float:left;-o-object-fit:contain;object-fit:contain;max-width:150px;-o-object-position:top;object-position:top}.entity-detail .entity-detail__img-origin{cursor:-webkit-zoom-in;cursor:zoom-in}.entity-detail .entity-detail__info{float:left;margin-left:20px;overflow:hidden;text-overflow:ellipsis;width:70%}.entity-detail .entity-detail__title{font-weight:bold}.entity-detail .entity-detail__title--secondary{color:#bbb}.entity-detail .entity-detail__fields{display:inline-block;vertical-align:top;width:46%;margin-left:2%}.entity-detail .entity-detail__fields div,.entity-detail .entity-detail__fields span{margin:1px 0}.entity-detail .entity-detail__fields+.tag-collection{margin-top:5px;margin-left:6px}.entity-detail .entity-detail__rating{position:relative;top:-5px}.entity-detail .entity-detail__rating-star{position:relative;left:-4px;top:3px}.entity-detail .entity-detail__rating-score{font-weight:bold}.entity-detail::after{content:' ';clear:both;display:table}.entity-desc{margin-bottom:28px}.entity-desc .entity-desc__title{display:inline-block;margin-bottom:8px}.entity-desc .entity-desc__content{overflow:hidden}.entity-desc .entity-desc__content--folded{max-height:202px}.entity-desc .entity-desc__unfold-button{display:-webkit-box;display:-ms-flexbox;display:flex;color:#00a1cc;background-color:transparent;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;text-align:center}.entity-desc .entity-desc__unfold-button--hidden{display:none}.entity-marks{margin-bottom:28px}.entity-marks .entity-marks__title{margin-bottom:8px;display:inline-block}.entity-marks .entity-marks__title>a{margin-right:5px}.entity-marks .entity-marks__title--stand-alone{margin-bottom:20px}.entity-marks .entity-marks__more-link{margin-left:5px}.entity-marks .entity-marks__mark{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-marks .entity-marks__mark:last-child{border:none}.entity-marks .entity-marks__mark--wider{padding:6px 0}.entity-marks .entity-marks__mark-content{margin-bottom:0}.entity-marks .entity-marks__mark-time{color:#ccc;margin-left:2px}.entity-marks .entity-marks__rating-star{position:relative;top:4px}.entity-reviews:first-child{margin-bottom:28px}.entity-reviews .entity-reviews__title{display:inline-block;margin-bottom:8px}.entity-reviews .entity-reviews__title>a{margin-right:5px}.entity-reviews .entity-reviews__title--stand-alone{margin-bottom:20px}.entity-reviews .entity-reviews__more-link{margin-left:5px}.entity-reviews .entity-reviews__review{margin:0;padding:3px 0;border-bottom:1px dashed #e5e5e5}.entity-reviews .entity-reviews__review:last-child{border:none}.entity-reviews .entity-reviews__review--wider{padding:6px 0}.entity-reviews .entity-reviews__review-time{color:#ccc;margin-left:2px}.dividing-line{height:0;width:100%;margin:40px 0 24px 0;border-top:solid 1px #ccc}.dividing-line.dividing-line--dashed{margin:0;margin-top:10px;margin-bottom:2px;border-top:1px dashed #e5e5e5}.entity-sort{position:relative;margin-bottom:30px}.entity-sort .entity-sort__label{font-size:large;display:inline-block;margin-bottom:20px}.entity-sort .entity-sort__more-link{margin-left:5px}.entity-sort .entity-sort__entity-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-ms-flex-wrap:wrap;flex-wrap:wrap}.entity-sort .entity-sort__entity{padding:0 10px;-ms-flex-preferred-size:20%;flex-basis:20%;text-align:center;display:inline-block;color:#606c76}.entity-sort .entity-sort__entity:hover{color:#00a1cc}.entity-sort .entity-sort__entity>a{color:inherit}.entity-sort .entity-sort__entity-img{height:110px}.entity-sort .entity-sort__entity-name{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.entity-sort--placeholder{border:dashed #bbb 4px}.entity-sort--hover{padding:10px;border:dashed #00a1cc 2px !important;border-radius:3px}.entity-sort--sortable{padding:10px;margin:10px 0;border:dashed #bbb 2px;cursor:all-scroll}.entity-sort--hidden{opacity:0.4}.entity-sort-control{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.entity-sort-control__button{margin-top:5px;margin-left:12px;padding:0 2px;cursor:pointer;color:#bbb}.entity-sort-control__button:hover{color:#00a1cc}.entity-sort-control__button:hover>.icon-save svg,.entity-sort-control__button:hover>.icon-edit svg{fill:#00a1cc}.entity-sort-control__button--float-right{position:absolute;top:4px;right:10px}.related-user-list .related-user-list__title{margin-bottom:20px}.related-user-list .related-user-list__user{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;margin-bottom:20px}.related-user-list .related-user-list__user-info{margin-left:15px;overflow:auto}.related-user-list .related-user-list__user-avatar{max-height:72px;min-width:72px}.review-head .review-head__title{display:inline-block;font-weight:bold}.review-head .review-head__body{margin-bottom:10px}.review-head .review-head__body::after{content:' ';clear:both;display:table}.review-head .review-head__info{float:left}.review-head .review-head__owner-link{color:#ccc}.review-head .review-head__owner-link:hover{color:#00a1cc}.review-head .review-head__time{color:#ccc}.review-head .review-head__rating-star{position:relative;top:3px;left:-1px}.review-head .review-head__actions{float:right}.review-head .review-head__action-link:not(:first-child){margin-left:5px}.tag-collection{margin-left:-9px}.tag-collection .tag-collection__tag{position:relative;display:block;float:left;color:white;background:#ccc;padding:5px;border-radius:.3rem;line-height:1.2em;font-size:80%;margin:3px}.tag-collection .tag-collection__tag a{color:white}.tag-collection .tag-collection__tag a:hover{color:#00a1cc}.track-carousel{position:relative;margin-top:5px}.track-carousel__content{overflow:auto;scroll-behavior:smooth;scrollbar-width:none;display:-webkit-box;display:-ms-flexbox;display:flex;margin:auto;-webkit-box-sizing:border-box;box-sizing:border-box;padding-bottom:10px}.track-carousel__content::-webkit-scrollbar{height:3px;width:1px;background-color:#e5e5e5}.track-carousel__content::-webkit-scrollbar-thumb{background-color:#bbb}.track-carousel__track{text-align:center;overflow:hidden;text-overflow:ellipsis;min-width:18%;max-width:18%;margin-right:2.5%}.track-carousel__track img{-o-object-fit:contain;object-fit:contain}.track-carousel__track-title{white-space:nowrap}.track-carousel__button{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-ms-flex-line-pack:center;align-content:center;background:white;border:none;padding:8px;border-radius:50%;outline:0;cursor:pointer;position:absolute;top:50%}.track-carousel__button--prev{left:0;-webkit-transform:translate(50%, -50%);transform:translate(50%, -50%)}.track-carousel__button--next{right:0;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%)}@media (max-width: 575.98px){.entity-list .entity-list__entity{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-bottom:30px}.entity-list .entity-list__entity-text{margin-left:0}.entity-list .entity-list__entity-img-wrapper{margin-bottom:8px}.entity-list .entity-list__entity-info{max-width:unset}.entity-list .entity-list__rating--empty+.entity-list__entity-info{max-width:70%}.entity-list .entity-list__entity-brief{-webkit-line-clamp:5}.entity-detail{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-detail .entity-detail__title{margin-bottom:5px}.entity-detail .entity-detail__info{margin-left:0;float:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%}.entity-detail .entity-detail__img{margin-bottom:24px;float:none;height:unset;max-width:170px}.entity-detail .entity-detail__fields{width:unset;margin-left:unset}.entity-detail .entity-detail__fields+.tag-collection{margin-left:-3px}.dividing-line{margin-top:24px}.entity-sort .entity-sort__entity{-ms-flex-preferred-size:50%;flex-basis:50%}.entity-sort .entity-sort__entity-img{height:130px}.review-head .review-head__info{float:unset}.review-head .review-head__actions{float:unset}.track-carousel__content{padding-bottom:10px}.track-carousel__track{min-width:31%;max-width:31%;margin-right:4.5%}}@media (max-width: 991.98px){.main-section-wrapper{padding:32px 28px 28px 28px}.entity-detail{display:-webkit-box;display:-ms-flexbox;display:flex}}.aside-section-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;padding:28px 25px 12px 25px;background-color:#f7f7f7;margin-bottom:30px;overflow:auto}.aside-section-wrapper--transparent{background-color:unset}.aside-section-wrapper--collapse{padding:unset}.add-entity-entries .add-entity-entries__entry{margin-bottom:10px}.add-entity-entries .add-entity-entries__label{font-size:1.2em;margin-bottom:8px}.add-entity-entries .add-entity-entries__button{line-height:unset;height:unset;padding:4px 15px;margin:5px}.action-panel{margin-bottom:20px}.action-panel .action-panel__label{font-weight:bold;margin-bottom:12px}.action-panel .action-panel__button-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.action-panel .action-panel__button-group--center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.action-panel .action-panel__button{line-height:unset;height:unset;padding:4px 15px;margin:0 5px}.mark-panel{margin-bottom:20px}.mark-panel .mark-panel__status{font-weight:bold}.mark-panel .mark-panel__rating-star{position:relative;top:2px}.mark-panel .mark-panel__actions{float:right}.mark-panel .mark-panel__actions form{display:inline}.mark-panel .mark-panel__time{color:#ccc;margin-bottom:10px}.mark-panel .mark-panel__clear{content:' ';clear:both;display:table}.review-panel .review-panel__label{font-weight:bold}.review-panel .review-panel__actions{float:right}.review-panel .review-panel__time{color:#ccc;margin-bottom:10px}.review-panel .review-panel__review-title{display:block;margin-bottom:15px;font-weight:bold}.review-panel .review-panel__clear{content:' ';clear:both;display:table}.user-profile .user-profile__header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;margin-bottom:15px}.user-profile .user-profile__avatar{width:72px}.user-profile .user-profile__username{font-size:large;margin-left:10px;margin-bottom:0}.user-profile .user-profile__report-link{color:#ccc}.user-relation .user-relation__label{display:inline-block;font-size:large;margin-bottom:10px}.user-relation .user-relation__more-link{margin-left:5px}.user-relation .user-relation__related-user-list{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.user-relation .user-relation__related-user{-ms-flex-preferred-size:25%;flex-basis:25%;padding:0px 3px;text-align:center;display:inline-block;overflow:hidden}.user-relation .user-relation__related-user>a:hover{color:#606c76}.user-relation .user-relation__related-user-avatar{width:48px}.user-relation .user-relation__related-user-name{color:inherit;overflow:hidden;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}.report-panel .report-panel__label{display:inline-block;margin-bottom:10px}.report-panel .report-panel__report{margin:2px 0}.report-panel .report-panel__user-link{margin:0 2px}.report-panel .report-panel__all-link{margin-left:5px}.relation-dropdown .relation-dropdown__button{display:none}.entity-card{display:-webkit-box;display:-ms-flexbox;display:flex;margin-bottom:10px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.entity-card--horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__img{height:150px}.entity-card .entity-card__rating-star{position:relative;top:4px;left:-3px}.entity-card .entity-card__rating-score{position:relative;top:1px;margin-left:2px}.entity-card .entity-card__title{margin-bottom:10px;margin-top:5px}.entity-card .entity-card__info-wrapper--horizontal{margin-left:20px}.entity-card .entity-card__img-wrapper{-ms-flex-preferred-size:100px;flex-basis:100px}@media (max-width: 575.98px){.add-entity-entries{display:block !important}.add-entity-entries .add-entity-entries__button{width:100%;margin:5px 0 5px 0}.aside-section-wrapper:first-child{margin-right:0 !important;margin-bottom:0 !important}.aside-section-wrapper--singular:first-child{margin-bottom:20px !important}.action-panel{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card--horizontal{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.entity-card .entity-card__info-wrapper{margin-left:10px !important}.entity-card .entity-card__info-wrapper--horizontal{margin-left:0 !important}}@media (max-width: 991.98px){.add-entity-entries{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around}.aside-section-wrapper{padding:24px 25px 10px 25px;margin-top:20px}.aside-section-wrapper:not(:last-child){margin-right:20px}.aside-section-wrapper--collapse{padding:24px 25px 10px 25px !important;margin-top:0;margin-bottom:0}.aside-section-wrapper--collapse:first-child{margin-right:0}.aside-section-wrapper--no-margin{margin:0}.action-panel{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.action-panel .action-panel__button-group{-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly}.relation-dropdown{margin-bottom:20px}.relation-dropdown .relation-dropdown__button{padding-bottom:10px;background-color:#f7f7f7;width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer}.relation-dropdown .relation-dropdown__button:focus{background-color:red}.relation-dropdown .relation-dropdown__button>.icon-arrow{-webkit-transition:-webkit-transform 0.3s;transition:-webkit-transform 0.3s;transition:transform 0.3s;transition:transform 0.3s, -webkit-transform 0.3s}.relation-dropdown .relation-dropdown__button:hover>.icon-arrow>svg{fill:#00a1cc}.relation-dropdown .relation-dropdown__button>.icon-arrow--expand{-webkit-transform:rotate(-180deg);transform:rotate(-180deg)}.relation-dropdown .relation-dropdown__button+.relation-dropdown__body--expand{max-height:500px;-webkit-transition:max-height 0.6s ease-in;transition:max-height 0.6s ease-in}.relation-dropdown .relation-dropdown__body{background-color:#f7f7f7;max-height:0;-webkit-transition:max-height 0.6s ease-out;transition:max-height 0.6s ease-out;overflow:hidden}.entity-card{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.entity-card .entity-card__info-wrapper{margin-left:30px}}.single-section-wrapper{padding:32px 36px;background-color:#f7f7f7;overflow:auto}.single-section-wrapper .single-section-wrapper__link--secondary{display:inline-block;color:#ccc;margin-bottom:20px}.single-section-wrapper .single-section-wrapper__link--secondary:hover{color:#00a1cc}.entity-form,.review-form{overflow:auto}.entity-form>input[type='email'],.entity-form>input[type='number'],.entity-form>input[type='password'],.entity-form>input[type='search'],.entity-form>input[type='tel'],.entity-form>input[type='text'],.entity-form>input[type='url'],.entity-form textarea,.review-form>input[type='email'],.review-form>input[type='number'],.review-form>input[type='password'],.review-form>input[type='search'],.review-form>input[type='tel'],.review-form>input[type='text'],.review-form>input[type='url'],.review-form textarea{width:100%}.entity-form img,.review-form img{display:block}.review-form .review-form__preview-button{color:#00a1cc;font-weight:bold;cursor:pointer}.review-form .review-form__fyi{color:#ccc}.review-form .review-form__main-content,.review-form textarea{margin-bottom:5px;resize:vertical;height:400px}.review-form .review-form__option{margin-top:24px;margin-bottom:10px}.review-form .review-form__option::after{content:' ';clear:both;display:table}.review-form .review-form__visibility-radio{float:left}.review-form .review-form__visibility-radio ul,.review-form .review-form__visibility-radio li,.review-form .review-form__visibility-radio label{display:inline}.review-form .review-form__visibility-radio label{font-size:normal}.review-form .review-form__visibility-radio input[type="radio"]{position:relative;top:2px}.review-form .review-form__share-checkbox{float:right}.review-form .review-form__share-checkbox input[type="checkbox"]{position:relative;top:2px}.report-form input,.report-form select{width:100%}@media (max-width: 575.98px){.review-form .review-form__visibility-radio{float:unset}.review-form .review-form__share-checkbox{float:unset;position:relative;left:-3px}}.markdownx-preview{min-height:100px}.markdownx-preview ul li{list-style:circle inside}.rating-star .jq-star{cursor:unset !important}.ms-parent>.ms-choice{margin-bottom:1.5rem;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #ccc;border-radius:.4rem;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:inherit;box-sizing:inherit;padding:.6rem 1.0rem;width:100%;height:30.126px}.ms-parent>.ms-choice:focus{border-color:#00a1cc}.ms-parent>.ms-choice>.icon-caret{top:15.5px}.ms-parent>.ms-choice>span{color:black;font-weight:initial;font-size:13.3333px;top:2.5px;left:2px}.ms-parent>.ms-choice>span:hover,.ms-parent>.ms-choice>span:focus{color:black}.ms-parent>.ms-drop>ul>li>label>span{margin-left:10px}.ms-parent>.ms-drop>ul>li>label>input{width:unset} diff --git a/common/static/sass/_Label.sass b/common/static/sass/_Label.sass index 1fd97c94..e80cad4b 100644 --- a/common/static/sass/_Label.sass +++ b/common/static/sass/_Label.sass @@ -7,6 +7,8 @@ $spotify-color-primary: #1ed760 $spotify-color-secondary: black $imdb-color-primary: #F5C518 $imdb-color-secondary: #121212 +$steam-color-primary: #1387b8 +$steam-color-secondary: #111d2e .source-label display: inline @@ -46,3 +48,9 @@ $imdb-color-secondary: #121212 color: $imdb-color-secondary border: none font-weight: bold + &.source-label__steam + background: linear-gradient(30deg, $steam-color-primary, $steam-color-secondary) + color: white + border: none + font-weight: 600 + padding-top: 2px diff --git a/common/templates/common/search_result.html b/common/templates/common/search_result.html index 85e08973..baec6b0d 100644 --- a/common/templates/common/search_result.html +++ b/common/templates/common/search_result.html @@ -65,7 +65,7 @@ {% endif %} - {% if not request.GET.c or request.GET.c != 'movie' and request.GET.c != 'book'%} + {% if not request.GET.c or not request.GET.c in categories %} [{{item.verbose_category_name}}] {% endif %} @@ -161,7 +161,7 @@ {% endif %} - {% if not request.GET.c or request.GET.c != 'movie' and request.GET.c != 'book'%} + {% if not request.GET.c or not request.GET.c in categories %} [{{item.verbose_category_name}}] {% endif %} @@ -221,6 +221,86 @@ {% endwith %} + {% elif item.category_name|lower == 'game' %} + + {% with game=item %} +
  • + +
    +
    + + {% if request.GET.q %} + {{ game.title | highlight:request.GET.q }} + {% else %} + {{ game.title }} + {% endif %} + + + {% if not request.GET.c or not request.GET.c in categories %} + [{{item.verbose_category_name}}] + {% endif %} + + {{ game.get_source_site_display }} + +
    + + {% if game.rating %} +
    + {{ game.rating }} + {% else %} +
    {% trans '暂无评分' %}
    + {% endif %} + + + + {% if game.other_title %}{% trans '别名' %} + {% for other_title in game.other_title %} + {{ other_title }}{% if not forloop.last %} {% endif %} + {% endfor %}/ + {% endif %} + + {% if game.developer %}{% trans '开发商' %} + {% for developer in game.developer %} + {{ developer }}{% if not forloop.last %} {% endif %} + {% endfor %}/ + {% endif %} + + {% if game.genre %}{% trans '类型' %} + {% for genre in game.genre %} + {{ genre }}{% if not forloop.last %} {% endif %} + {% endfor %}/ + {% endif %} + + {% if game.platform %}{% trans '平台' %} + {% for platform in game.platform %} + {{ platform }}{% if not forloop.last %} {% endif %} + {% endfor %}/ + {% endif %} + + +

    + {{ game.brief }} +

    +
    + {% for tag_dict in game.tag_list %} + {% for k, v in tag_dict.items %} + {% if k == 'content' %} + + {{ v }} + + {% endif %} + {% endfor %} + {% endfor %} +
    +
    + +
  • + {% endwith %} + {% elif item.category_name|lower == 'album' or item.category_name|lower == 'song' %} {% with music=item %} @@ -260,7 +340,7 @@ {% endif %} - {% if not request.GET.c or request.GET.c != 'music' and request.GET.c != 'book' and request.GET.c != 'music' %} + {% if not request.GET.c or not request.GET.c in categories %} [{{item.verbose_category_name}}] {% endif %} @@ -371,7 +451,7 @@
    {% trans '没有想要的结果?' %}
    - {% if request.GET.c %} + {% if request.GET.c and request.GET.c in categories %} {% if request.GET.c|lower == 'book' %} @@ -387,17 +467,6 @@ {% elif request.GET.c|lower == 'music' %} -
    - - - - {% else %} - - - - - - @@ -405,6 +474,12 @@ + {% elif request.GET.c|lower == 'game' %} + + + + + {% endif %} {% else %} @@ -420,10 +495,13 @@ + + + {% endif %}
    - {% if request.GET.c %} + {% if request.GET.c and request.GET.c in categories %} {% if request.GET.c|lower == 'book' %} @@ -443,6 +521,15 @@ + {% elif request.GET.c|lower == 'game' %} + +
    + {% trans '或者(≖ ◡ ≖)✧' %} +
    + + + + {% elif request.GET.c|lower == 'music' %}
    @@ -451,22 +538,7 @@ - - {% else %} - -
    - {% trans '或从表瓣剽取' %} -
    - - - - - - - - - - + {% endif %} {% else %} @@ -483,6 +555,9 @@ + + + {% endif %}
    diff --git a/common/templates/partial/_navbar.html b/common/templates/partial/_navbar.html index 2ec9eb7d..290e027a 100644 --- a/common/templates/partial/_navbar.html +++ b/common/templates/partial/_navbar.html @@ -18,6 +18,7 @@ +
    diff --git a/common/urls.py b/common/urls.py index 22843ee7..6de4f66a 100644 --- a/common/urls.py +++ b/common/urls.py @@ -1,7 +1,6 @@ from django.urls import path from .views import * - app_name = 'common' urlpatterns = [ path('', home), diff --git a/common/views.py b/common/views.py index 48c87047..c028184a 100644 --- a/common/views.py +++ b/common/views.py @@ -12,137 +12,22 @@ from django.db.models import Q, Count from django.http import HttpResponseBadRequest from books.models import Book from movies.models import Movie +from games.models import Game from music.models import Album, Song, AlbumMark, SongMark from users.models import Report, User, Preference from mastodon.decorators import mastodon_request_included +from users.views import home as user_home from common.models import MarkStatusEnum from common.utils import PageLinksGenerator from common.scraper import scraper_registry +from common.config import * from management.models import Announcement - -# how many books have in each set at the home page -BOOKS_PER_SET = 5 - -# how many movies have in each set at the home page -MOVIES_PER_SET = 5 - -MUSIC_PER_SET = 5 - -# how many items are showed in one search result page -ITEMS_PER_PAGE = 20 - -# how many pages links in the pagination -PAGE_LINK_NUMBER = 7 - -# max tags on list page -TAG_NUMBER_ON_LIST = 5 - logger = logging.getLogger(__name__) @login_required def home(request): - """ - Should be merged to users:home in the future - """ - if request.method == 'GET': - - # really shitty code here - - unread_announcements = Announcement.objects.filter( - pk__gt=request.user.read_announcement_index).order_by('-pk') - try: - request.user.read_announcement_index = Announcement.objects.latest('pk').pk - request.user.save(update_fields=['read_announcement_index']) - except ObjectDoesNotExist as e: - # when there is no annoucenment - pass - - do_book_marks = request.user.user_bookmarks.filter( - status=MarkStatusEnum.DO).order_by("-edited_time") - do_books_more = True if do_book_marks.count() > BOOKS_PER_SET else False - - wish_book_marks = request.user.user_bookmarks.filter( - status=MarkStatusEnum.WISH).order_by("-edited_time") - wish_books_more = True if wish_book_marks.count() > BOOKS_PER_SET else False - - collect_book_marks = request.user.user_bookmarks.filter( - status=MarkStatusEnum.COLLECT).order_by("-edited_time") - collect_books_more = True if collect_book_marks.count() > BOOKS_PER_SET else False - - - do_movie_marks = request.user.user_moviemarks.filter( - status=MarkStatusEnum.DO).order_by("-edited_time") - do_movies_more = True if do_movie_marks.count() > MOVIES_PER_SET else False - - wish_movie_marks = request.user.user_moviemarks.filter( - status=MarkStatusEnum.WISH).order_by("-edited_time") - wish_movies_more = True if wish_movie_marks.count() > MOVIES_PER_SET else False - - collect_movie_marks = request.user.user_moviemarks.filter( - status=MarkStatusEnum.COLLECT).order_by("-edited_time") - collect_movies_more = True if collect_movie_marks.count() > MOVIES_PER_SET else False - - do_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) \ - + list(request.user.user_albummarks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) - do_music_more = True if len(do_music_marks) > MUSIC_PER_SET else False - do_music_marks = sorted(do_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] - - wish_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) \ - + list(request.user.user_albummarks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) - wish_music_more = True if len(wish_music_marks) > MUSIC_PER_SET else False - wish_music_marks = sorted(wish_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] - - collect_music_marks = list(request.user.user_songmarks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) \ - + list(request.user.user_albummarks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) - collect_music_more = True if len(collect_music_marks) > MUSIC_PER_SET else False - collect_music_marks = sorted(collect_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] - - for mark in do_music_marks + wish_music_marks + collect_music_marks: - # for template convenience - if mark.__class__ == AlbumMark: - mark.type = "album" - else: - mark.type = "song" - - reports = Report.objects.order_by('-submitted_time').filter(is_read=False) - # reports = Report.objects.latest('submitted_time').filter(is_read=False) - - try: - layout = request.user.preference.get_serialized_home_layout() - except ObjectDoesNotExist: - Preference.objects.create(user=request.user) - layout = request.user.preference.get_serialized_home_layout() - - return render( - request, - 'common/home.html', - { - 'do_book_marks': do_book_marks[:BOOKS_PER_SET], - 'wish_book_marks': wish_book_marks[:BOOKS_PER_SET], - 'collect_book_marks': collect_book_marks[:BOOKS_PER_SET], - 'do_books_more': do_books_more, - 'wish_books_more': wish_books_more, - 'collect_books_more': collect_books_more, - 'do_movie_marks': do_movie_marks[:MOVIES_PER_SET], - 'wish_movie_marks': wish_movie_marks[:MOVIES_PER_SET], - 'collect_movie_marks': collect_movie_marks[:MOVIES_PER_SET], - 'do_movies_more': do_movies_more, - 'wish_movies_more': wish_movies_more, - 'collect_movies_more': collect_movies_more, - 'do_music_marks': do_music_marks, - 'wish_music_marks': wish_music_marks, - 'collect_music_marks': collect_music_marks, - 'do_music_more': do_music_more, - 'wish_music_more': wish_music_more, - 'collect_music_more': collect_music_more, - 'reports': reports, - 'unread_announcements': unread_announcements, - 'layout': layout, - } - ) - else: - return HttpResponseBadRequest() + return user_home(request, request.user.id) @login_required @@ -266,6 +151,51 @@ def search(request): ordered_queryset = list(queryset) return ordered_queryset + def game_param_handler(**kwargs): + # keywords + keywords = kwargs.get('keywords') + # tag + tag = kwargs.get('tag') + + query_args = [] + q = Q() + + for keyword in keywords: + q = q | Q(title__icontains=keyword) + q = q | Q(other_title__icontains=keyword) + q = q | Q(developer__icontains=keyword) + q = q | Q(publisher__icontains=keyword) + if tag: + q = q & Q(game_tags__content__iexact=tag) + + query_args.append(q) + queryset = Game.objects.filter(*query_args).distinct() + + def calculate_similarity(game): + if keywords: + # search by name + developer_dump = ' '.join(game.developer) + publisher_dump = ' '.join(game.publisher) + similarity, n = 0, 0 + for keyword in keywords: + similarity += 1/2 * SequenceMatcher(None, keyword, game.title).quick_ratio() + + 1/4 * SequenceMatcher(None, keyword, game.other_title).quick_ratio() + + 1/16 * SequenceMatcher(None, keyword, developer_dump).quick_ratio() + + 1/16 * SequenceMatcher(None, keyword, publisher_dump).quick_ratio() + n += 1 + game.similarity = similarity / n + elif tag: + # search by single tag + game.similarity = 0 if game.rating_number is None else game.rating_number + else: + game.similarity = 0 + return game.similarity + if len(queryset) > 0: + ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True) + else: + ordered_queryset = list(queryset) + return ordered_queryset + def music_param_handler(**kwargs): # keywords keywords = kwargs.get('keywords') @@ -329,8 +259,9 @@ def search(request): book_queryset = book_param_handler(**kwargs) movie_queryset = movie_param_handler(**kwargs) music_queryset = music_param_handler(**kwargs) + game_queryset = game_param_handler(**kwargs) ordered_queryset = sorted( - book_queryset + movie_queryset + music_queryset, + book_queryset + movie_queryset + music_queryset + game_queryset, key=operator.attrgetter('similarity'), reverse=True ) @@ -340,10 +271,13 @@ def search(request): 'book': book_param_handler, 'movie': movie_param_handler, 'music': music_param_handler, + 'game': game_param_handler, 'all': all_param_handler, '': all_param_handler } + categories = [k for k in param_handler.keys() if not k in ['all', '']] + try: queryset = param_handler[category]( keywords=keywords, @@ -367,6 +301,7 @@ def search(request): "common/search_result.html", { "items": items, + "categories": categories, } ) diff --git a/games/__init__.py b/games/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/games/admin.py b/games/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/games/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/games/apps.py b/games/apps.py new file mode 100644 index 00000000..b74f62c9 --- /dev/null +++ b/games/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GamesConfig(AppConfig): + name = 'games' diff --git a/games/forms.py b/games/forms.py new file mode 100644 index 00000000..5aa53330 --- /dev/null +++ b/games/forms.py @@ -0,0 +1,98 @@ +from django import forms +from django.contrib.postgres.forms import SimpleArrayField +from django.utils.translation import gettext_lazy as _ +from .models import Game, GameMark, GameReview +from common.models import MarkStatusEnum +from common.forms import * + + +def GameMarkStatusTranslator(status): + trans_dict = { + MarkStatusEnum.DO.value: _("在玩"), + MarkStatusEnum.WISH.value: _("想玩"), + MarkStatusEnum.COLLECT.value: _("玩过") + } + return trans_dict[status] + + +class GameForm(forms.ModelForm): + # id = forms.IntegerField(required=False, widget=forms.HiddenInput()) + + other_info = JSONField(required=False, label=_("其他信息")) + + class Meta: + model = Game + fields = [ + 'title', + 'source_site', + 'source_url', + 'other_title', + 'developer', + 'publisher', + 'release_date', + 'genre', + 'platform', + 'cover', + 'brief', + 'other_info' + ] + + widgets = { + 'other_title': forms.TextInput(attrs={'placeholder': _("多个别名使用英文逗号分隔")}), + 'developer': forms.TextInput(attrs={'placeholder': _("多个开发商使用英文逗号分隔")}), + 'publisher': forms.TextInput(attrs={'placeholder': _("多个发行商使用英文逗号分隔")}), + 'genre': forms.TextInput(attrs={'placeholder': _("多个类型使用英文逗号分隔")}), + 'platform': forms.TextInput(attrs={'placeholder': _("多个平台使用英文逗号分隔")}), + 'cover': PreviewImageInput(), + } + + +class GameMarkForm(MarkForm): + + STATUS_CHOICES = [(v, GameMarkStatusTranslator(v)) + for v in MarkStatusEnum.values] + + status = forms.ChoiceField( + label=_(""), + widget=forms.RadioSelect(), + choices=STATUS_CHOICES + ) + + class Meta: + model = GameMark + fields = [ + 'id', + 'game', + 'status', + 'rating', + 'text', + 'is_private', + ] + labels = { + 'rating': _("评分"), + } + widgets = { + 'game': forms.TextInput(attrs={"hidden": ""}), + } + + +class GameReviewForm(ReviewForm): + + class Meta: + model = GameReview + fields = [ + 'id', + 'game', + 'title', + 'content', + 'is_private' + ] + labels = { + 'book': "", + 'title': _("标题"), + 'content': _("正文"), + 'share_to_mastodon': _("分享到长毛象") + } + widgets = { + 'game': forms.TextInput(attrs={"hidden": ""}), + } diff --git a/games/models.py b/games/models.py new file mode 100644 index 00000000..06a631f3 --- /dev/null +++ b/games/models.py @@ -0,0 +1,128 @@ +import uuid +import django.contrib.postgres.fields as postgres +from django.utils.translation import ugettext_lazy as _ +from django.db import models +from django.core.serializers.json import DjangoJSONEncoder +from django.shortcuts import reverse +from common.models import Entity, Mark, Review, Tag +from common.utils import ChoicesDictGenerator +from boofilsic.settings import GAME_MEDIA_PATH_ROOT, DEFAULT_GAME_IMAGE +from django.utils import timezone + + +def game_cover_path(instance, filename): + ext = filename.split('.')[-1] + filename = "%s.%s" % (uuid.uuid4(), ext) + root = '' + if GAME_MEDIA_PATH_ROOT.endswith('/'): + root = GAME_MEDIA_PATH_ROOT + else: + root = GAME_MEDIA_PATH_ROOT + '/' + return root + timezone.now().strftime('%Y/%m/%d') + f'{filename}' + + +class Game(Entity): + """ + """ + + title = models.CharField(_("名称"), max_length=500) + + other_title = postgres.ArrayField( + models.CharField(blank=True,default='', max_length=500), + null=True, + blank=True, + default=list, + verbose_name=_("别名") + ) + + developer = postgres.ArrayField( + models.CharField(blank=True, default='', max_length=500), + null=True, + blank=True, + default=list, + verbose_name=_("开发商") + ) + + publisher = postgres.ArrayField( + models.CharField(blank=True, default='', max_length=500), + null=True, + blank=True, + default=list, + verbose_name=_("发行商") + ) + + release_date = models.DateField( + _('发行日期'), + auto_now=False, + auto_now_add=False, + null=True, + blank=True + ) + + genre = postgres.ArrayField( + models.CharField(blank=True, default='', max_length=50), + null=True, + blank=True, + default=list, + verbose_name=_("类型") + ) + + platform = postgres.ArrayField( + models.CharField(blank=True, default='', max_length=50), + null=True, + blank=True, + default=list, + verbose_name=_("平台") + ) + + cover = models.ImageField(_("封面"), upload_to=game_cover_path, default=DEFAULT_GAME_IMAGE, blank=True) + + + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("games:retrieve", args=[self.id]) + + def get_tags_manager(self): + return self.game_tags + + @property + def verbose_category_name(self): + return _("游戏") + + +class GameMark(Mark): + game = models.ForeignKey( + Game, on_delete=models.CASCADE, related_name='game_marks', null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['owner', 'game'], name='unique_game_mark') + ] + + +class GameReview(Review): + game = models.ForeignKey( + Game, on_delete=models.CASCADE, related_name='game_reviews', null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['owner', 'game'], name='unique_game_review') + ] + + +class GameTag(Tag): + game = models.ForeignKey( + Game, on_delete=models.CASCADE, related_name='game_tags', null=True) + mark = models.ForeignKey( + GameMark, on_delete=models.CASCADE, related_name='gamemark_tags', null=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['content', 'mark'], name="unique_gamemark_tag") + ] diff --git a/games/templates/games/create_update.html b/games/templates/games/create_update.html new file mode 100644 index 00000000..086f2ad4 --- /dev/null +++ b/games/templates/games/create_update.html @@ -0,0 +1,95 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - ' %}{{ title }} + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    + {% trans '>>> 试试一键剽取~ <<<' %} +
    + {% csrf_token %} + {{ form.media }} + {% for field in form %} + + {% if field.name == 'release_date' %} + {{ field.label_tag }} + + + + {% else %} + {% if field.name != 'id' %} + {{ field.label_tag }} + {% endif %} + {{ field }} + {% endif %} + + {% endfor %} + + +
    +
    + +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + \ No newline at end of file diff --git a/games/templates/games/create_update_review.html b/games/templates/games/create_update_review.html new file mode 100644 index 00000000..54391b6d --- /dev/null +++ b/games/templates/games/create_update_review.html @@ -0,0 +1,130 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} + + + + + + + {% trans 'NiceDB - ' %}{{ title }} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    + + + +
    +
    + +
    + {{ game.title }} + + {{ game.get_source_site_display }} +
    + +
    + {% if game.genre %}{% trans '类型:' %} + {% for genre in game.genre %} + {{ genre }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    + {% if game.developer %}{% trans '开发商:' %} + {% for developer in game.developer %} + {{ developer }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    {% if game.release_date %} + {% trans '发行日期:' %}{{ game.release_date }} + {% endif %} +
    + + {% if game.rating %} + {% trans '评分:' %} + {{ game.rating }} + {% endif %} +
    +
    +
    + +
    + {% csrf_token %} + {{ form.game }} +
    + {{ form.title.label }} +
    + {{ form.title }} +
    + + {{ form.content.label }} + + + {% trans '预览' %} + +
    +
    + {{ form.content }} +
    +
    {% trans '不知道什么是Markdown?可以参考' %}{% trans '这里' %}
    +
    +
    + + {{ form.is_private.label }}{{ form.is_private }} +
    + +
    +
    + +
    + {{ form.media }} +
    + +
    + +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/games/templates/games/delete.html b/games/templates/games/delete.html new file mode 100644 index 00000000..008a561b --- /dev/null +++ b/games/templates/games/delete.html @@ -0,0 +1,105 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} + + + + + + + {% trans 'NiceDB - 删除电影/剧集' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    {% trans '确认删除这个游戏吗?相关评论和标记将一并删除。' %}
    + +
    +
    + + + +
    +
    + +
    + {{ game.title }} + {{ game.get_source_site_display }} +
    + + {% if game.rating %} + {% trans '评分:' %} + + {{ game.rating }} + {% else %} + {% trans '评分:暂无评分' %} + {% endif %} + + {% if game.last_editor %} +
    + {% trans '最近编辑者:' %} + + {{ game.last_editor | default:"" }} + +
    + {% endif %} + +
    {% trans '上次编辑时间:' %}{{ game.edited_time }}
    + + {% if game.game_marks.all %} +
    {% trans '这个条目有' %} {{ game.game_marks.count }} 个标记
    + {% endif %} + {% if game.game_reviews.all %} +
    {% trans '这个条目有' %} {{ game.game_reviews.count }} 个评论
    + {% endif %} + +
    +
    +
    +
    +
    + {% csrf_token %} + +
    + +
    +
    +
    +
    + +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/games/templates/games/delete_review.html b/games/templates/games/delete_review.html new file mode 100644 index 00000000..b24fc5d2 --- /dev/null +++ b/games/templates/games/delete_review.html @@ -0,0 +1,107 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - 删除评论' %} + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    {% trans '确认删除这篇评论吗?' %}
    + +
    + + +
    + +
    + {{ review.title }} +
    + {% if review.is_private %} + + + + {% endif %} + +
    +
    + + {{ review.owner.username }} + + {% if mark %} + + {% if mark.rating %} + + {% endif %} + + {% endif %} + + {{ review.edited_time }} + +
    +
    + + +
    +
    + {{ form.content }} +
    + {{ form.media }} + +
    + +
    +
    + {% csrf_token %} + +
    + +
    +
    +
    +
    + +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + \ No newline at end of file diff --git a/games/templates/games/detail.html b/games/templates/games/detail.html new file mode 100644 index 00000000..a5b18811 --- /dev/null +++ b/games/templates/games/detail.html @@ -0,0 +1,420 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load strip_scheme %} +{% load thumb %} + + + + + + + + + + + + + + {% trans 'NiceDB - 游戏详情' %} | {{ game.title }} + + + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    + + + {{ game.title }} + + +
    +
    + {{ game.title }} + {{ game.get_source_site_display }} +
    + +
    +
    + {% if game.rating %} + + {{ game.rating }} + {% else %} + {% trans '评分:暂无评分' %} + {% endif %} +
    + +
    {% if game.other_title %}{% trans '别名:' %} + {% for other_title in game.other_title %} + 5 %}style="display: none;" {% endif %}> + {{ other_title }} + {% if not forloop.last %} / {% endif %} + + {% endfor %} + {% if game.other_title|length > 5 %} + {% trans '更多' %} + + {% endif %} + {% endif %} +
    + +
    + {% if game.genre %}{% trans '类型:' %} + {% for genre in game.genre %} + {{ genre }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    + {% if game.developer %}{% trans '开发商:' %} + {% for developer in game.developer %} + {{ developer }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    + {% if game.publisher %}{% trans '发行商:' %} + {% for publisher in game.publisher %} + {{ publisher }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    +
    +
    {% if game.release_date %} + {% trans '发行日期:' %}{{ game.release_date }} + {% endif %} +
    +
    + {% if game.platform %}{% trans '平台:' %} + {% for platform in game.platform %} + {{ platform }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + + {% if game.other_info %} + {% for k, v in game.other_info.items %} +
    + {{ k }}:{{ v | urlize }} +
    + {% endfor %} + {% endif %} + + + {% if game.last_editor %} +
    {% trans '最近编辑者:' %}{{ game.last_editor | default:"" }}
    + {% endif %} + +
    + {% trans '编辑这个游戏' %} + + {% if user.is_staff %} + / {% trans '删除' %} + {% endif %} +
    +
    + +
    + + {% for tag_dict in game_tag_list %} + {% for k, v in tag_dict.items %} + {% if k == 'content' %} + + {{ v }} + + {% endif %} + {% endfor %} + {% endfor %} + +
    +
    +
    +
    + {% if game.brief %} +
    +
    {% trans '简介' %}
    + +

    {{ game.brief | linebreaksbr }}

    + + +
    + {% endif %} + + +
    + +
    {% trans '这个游戏的标记' %}
    + {% if mark_list_more %} + {% trans '更多' %} + {% endif %} + {% if mark_list %} +
      + {% for others_mark in mark_list %} +
    • + {{ others_mark.owner.username }} + {{ others_mark.get_status_display }} + {% if others_mark.rating %} + + {% endif %} + {% if others_mark.is_private %} + + {% endif %} + {{ others_mark.edited_time }} + {% if others_mark.text %} +

      {{ others_mark.text }}

      + {% endif %} +
    • + {% endfor %} +
    + {% else %} +
    {% trans '暂无标记' %}
    + {% endif %} +
    +
    +
    {% trans '这个游戏的评论' %}
    + + {% if review_list_more %} + {% trans '更多' %} + {% endif %} + {% if review_list %} + + {% else %} +
    {% trans '暂无评论' %}
    + {% endif %} +
    +
    +
    + +
    +
    + + {% if mark %} +
    + + {% trans '我' %}{{ mark.get_status_display }} + {% if mark.status == status_enum.DO.value or mark.status == status_enum.COLLECT.value%} + {% if mark.rating %} + + {% endif %} + {% endif %} + {% if mark.is_private %} + + {% endif %} + + {% trans '修改' %} +
    + {% csrf_token %} + {% trans '删除' %} +
    +
    +
    + +
    {{ mark.edited_time }}
    + + {% if mark.text %} +

    {{ mark.text }}

    + {% endif %} +
    + + {% for tag in mark_tags %} + {{ tag }} + {% endfor %} + +
    +
    + {% else %} +
    + +
    {% trans '标记这个游戏' %}
    +
    + + + +
    +
    + {% endif %} + +
    + +
    + {% if review %} +
    + + {% trans '我的评论' %} + {% if review.is_private %} + + {% endif %} + + + {% trans '编辑' %} + {% trans '删除' %} + + +
    {{ review.edited_time }}
    + + + {{ review.title }} + +
    + {% else %} + +
    +
    {% trans '我的评论' %}
    + +
    + + {% endif %} +
    + +
    +
    +
    + +
    + {% include "partial/_footer.html" %} +
    + +
    + + + +
    +
    + + + + + + diff --git a/games/templates/games/mark_list.html b/games/templates/games/mark_list.html new file mode 100644 index 00000000..45071f89 --- /dev/null +++ b/games/templates/games/mark_list.html @@ -0,0 +1,165 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} +{% load thumb %} + + + + + + + {% trans 'NiceDB - ' %}{{ game.title }}{% trans '的标记' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {{ game.title }}{% trans ' 的标记' %} +
    +
      + + {% for mark in marks %} + +
    • + {{ mark.owner.username }} + {{ mark.get_status_display }} + {% if mark.rating %} + + {% endif %} + {% if mark.is_private %} + + + + {% endif %} + {{ mark.edited_time }} + {% if mark.text %} +

      {{ mark.text }}

      + {% endif %} +
    • + + {% empty %} +
      + {% trans '无结果' %} +
      + {% endfor %} + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + {{ game.title }} + + {{ game.get_source_site_display }} +
    + +
    + {% if game.genre %}{% trans '类型:' %} + {% for genre in game.genre %} + {{ genre }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    + {% if game.developer %}{% trans '开发商:' %} + {% for developer in game.developer %} + {{ developer }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    {% if game.release_date %} + {% trans '发行日期:' %}{{ game.release_date }} + {% endif %} +
    + + {% if game.rating %} + {% trans '评分:' %} + {{ game.rating }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/games/templates/games/review_detail.html b/games/templates/games/review_detail.html new file mode 100644 index 00000000..1ffab003 --- /dev/null +++ b/games/templates/games/review_detail.html @@ -0,0 +1,152 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} + + + + + + + + + + + + {% trans 'NiceDB - 评论详情' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {{ review.title }} +
    + {% if review.is_private %} + + + + {% endif %} +
    +
    + + {{ review.owner.username }} + + {% if mark %} + + {% if mark.rating %} + + {% endif %} + + {% endif %} + + {{ review.edited_time }} + +
    +
    + {% if request.user == review.owner %} + {% trans '编辑' %} + {% trans '删除' %} + {% endif %} +
    +
    + +
    + {{ form.content }} +
    + {{ form.media }} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + + {{ game.title }} + + + + {{ game.get_source_site_display }} + + +
    + +
    + {% if game.genre %}{% trans '类型:' %} + {% for genre in game.genre %} + {{ genre }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    + {% if game.developer %}{% trans '开发商:' %} + {% for developer in game.developer %} + {{ developer }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    {% if game.release_date %} + {% trans '发行日期:' %}{{ game.release_date }} + {% endif %} +
    + + {% if game.rating %} + {% trans '评分:' %} + {{ game.rating }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/games/templates/games/review_list.html b/games/templates/games/review_list.html new file mode 100644 index 00000000..10b01d43 --- /dev/null +++ b/games/templates/games/review_list.html @@ -0,0 +1,153 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load highlight %} +{% load thumb %} + + + + + + + {% trans 'NiceDB - ' %}{{ game.title }}{% trans '的评论' %} + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {{ game.title }}{% trans ' 的评论' %} +
    +
      + + {% for review in reviews %} + +
    • + + {{ review.owner.username }} + {% if review.is_private %} + + {% endif %} + {{ review.edited_time }} + + + {{ review.title }} + +
    • + {% empty %} +
      {% trans '无结果' %}
      + {% endfor %} + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + {{ game.title }} + + {{ game.get_source_site_display }} +
    + +
    + {% if game.genre %}{% trans '类型:' %} + {% for genre in game.genre %} + {{ genre }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    + {% if game.developer %}{% trans '开发商:' %} + {% for developer in game.developer %} + {{ developer }}{% if not forloop.last %} / {% endif %} + {% endfor %} + {% endif %} +
    + +
    {% if game.release_date %} + {% trans '发行日期:' %}{{ game.release_date }} + {% endif %} +
    + + {% if game.rating %} + {% trans '评分:' %} + {{ game.rating }} + {% endif %} +
    + +
    +
    +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + {% comment %} + + + + + {% endcomment %} + + + + + + diff --git a/games/templates/games/scrape.html b/games/templates/games/scrape.html new file mode 100644 index 00000000..87f2efa8 --- /dev/null +++ b/games/templates/games/scrape.html @@ -0,0 +1,112 @@ +{% load static %} +{% load i18n %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} + + + + + + + {% trans 'NiceDB - 从豆瓣获取数据' %} + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    +
    + {% trans '根据豆瓣内容填写下方表单' %} +
    + +
    +
    +
    + {% csrf_token %} + + {{ form.media }} + + {% for field in form %} + + {% if field.name == 'release_date' %} + {{ field.label_tag }} + + + + {% else %} + {% if field.name != 'id' %} + {{ field.label_tag }} + {% endif %} + {{ field }} + {% endif %} + + {% endfor %} + +
    + {% trans '剽取!' %} +
    +
    +
    +
    + +
    +
    +
    + {% trans '复制详情页链接' %} +
    +
    + {% csrf_token %} + + +
    +
    + +
    +
    +
    + +
    + {% include "partial/_footer.html" %} + +
    + + + + + diff --git a/games/tests.py b/games/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/games/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/games/urls.py b/games/urls.py new file mode 100644 index 00000000..ff984632 --- /dev/null +++ b/games/urls.py @@ -0,0 +1,23 @@ +from django.urls import path +from .views import * + + +app_name = 'games' +urlpatterns = [ + path('create/', create, name='create'), + path('/', retrieve, name='retrieve'), + path('update//', update, name='update'), + path('delete//', delete, name='delete'), + path('mark/', create_update_mark, name='create_update_mark'), + path('/mark/list/', + retrieve_mark_list, name='retrieve_mark_list'), + path('mark/delete//', delete_mark, name='delete_mark'), + path('/review/create/', create_review, name='create_review'), + path('review/update//', update_review, name='update_review'), + path('review/delete//', delete_review, name='delete_review'), + path('review//', retrieve_review, name='retrieve_review'), + path('/review/list/', + retrieve_review_list, name='retrieve_review_list'), + path('scrape/', scrape, name='scrape'), + path('click_to_scrape/', click_to_scrape, name='click_to_scrape'), +] diff --git a/games/views.py b/games/views.py new file mode 100644 index 00000000..f69e4918 --- /dev/null +++ b/games/views.py @@ -0,0 +1,591 @@ +import logging +from django.shortcuts import render, get_object_or_404, redirect, reverse +from django.contrib.auth.decorators import login_required, permission_required +from django.utils.translation import gettext_lazy as _ +from django.http import HttpResponseBadRequest, HttpResponseServerError +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db import IntegrityError, transaction +from django.db.models import Count +from django.utils import timezone +from django.core.paginator import Paginator +from mastodon import mastodon_request_included +from mastodon.api import check_visibility, post_toot, TootVisibilityEnum +from mastodon.utils import rating_to_emoji +from common.utils import PageLinksGenerator +from common.views import PAGE_LINK_NUMBER, jump_or_scrape +from common.models import SourceSiteEnum +from .models import * +from .forms import * +from boofilsic.settings import MASTODON_TAGS + + +logger = logging.getLogger(__name__) +mastodon_logger = logging.getLogger("django.mastodon") + + +# how many marks showed on the detail page +MARK_NUMBER = 5 +# how many marks at the mark page +MARK_PER_PAGE = 20 +# how many reviews showed on the detail page +REVIEW_NUMBER = 5 +# how many reviews at the mark page +REVIEW_PER_PAGE = 20 +# max tags on detail page +TAG_NUMBER = 10 + + +# public data +########################### +@login_required +def create(request): + if request.method == 'GET': + form = GameForm() + return render( + request, + 'games/create_update.html', + { + 'form': form, + 'title': _('添加游戏'), + 'submit_url': reverse("games:create"), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + elif request.method == 'POST': + if request.user.is_authenticated: + # only local user can alter public data + form = GameForm(request.POST, request.FILES) + if form.is_valid(): + form.instance.last_editor = request.user + try: + with transaction.atomic(): + form.save() + if form.instance.source_site == SourceSiteEnum.IN_SITE.value: + real_url = form.instance.get_absolute_url() + form.instance.source_url = real_url + form.instance.save() + except IntegrityError as e: + logger.error(e.__str__()) + return HttpResponseServerError("integrity error") + return redirect(reverse("games:retrieve", args=[form.instance.id])) + else: + return render( + request, + 'games/create_update.html', + { + 'form': form, + 'title': _('添加游戏'), + 'submit_url': reverse("games:create"), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + else: + return redirect(reverse("users:login")) + else: + return HttpResponseBadRequest() + + +@login_required +def update(request, id): + if request.method == 'GET': + game = get_object_or_404(Game, pk=id) + form = GameForm(instance=game) + page_title = _('修改游戏') + return render( + request, + 'games/create_update.html', + { + 'form': form, + 'title': page_title, + 'submit_url': reverse("games:update", args=[game.id]), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + elif request.method == 'POST': + game = get_object_or_404(Game, pk=id) + form = GameForm(request.POST, request.FILES, instance=game) + page_title = _("修改游戏") + if form.is_valid(): + form.instance.last_editor = request.user + form.instance.edited_time = timezone.now() + try: + with transaction.atomic(): + form.save() + if form.instance.source_site == SourceSiteEnum.IN_SITE.value: + real_url = form.instance.get_absolute_url() + form.instance.source_url = real_url + form.instance.save() + except IntegrityError as e: + logger.error(e.__str__()) + return HttpResponseServerError("integrity error") + else: + return render( + request, + 'games/create_update.html', + { + 'form': form, + 'title': page_title, + 'submit_url': reverse("games:update", args=[game.id]), + # provided for frontend js + 'this_site_enum_value': SourceSiteEnum.IN_SITE.value, + } + ) + return redirect(reverse("games:retrieve", args=[form.instance.id])) + + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +# @login_required +def retrieve(request, id): + if request.method == 'GET': + game = get_object_or_404(Game, pk=id) + mark = None + mark_tags = None + review = None + + # retreive tags + game_tag_list = game.game_tags.values('content').annotate( + tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER] + + # retrieve user mark and initialize mark form + try: + if request.user.is_authenticated: + mark = GameMark.objects.get(owner=request.user, game=game) + except ObjectDoesNotExist: + mark = None + if mark: + mark_tags = mark.gamemark_tags.all() + mark.get_status_display = GameMarkStatusTranslator(mark.status) + mark_form = GameMarkForm(instance=mark, initial={ + 'tags': mark_tags + }) + else: + mark_form = GameMarkForm(initial={ + 'game': game, + 'tags': mark_tags + }) + + # retrieve user review + try: + if request.user.is_authenticated: + review = GameReview.objects.get( + owner=request.user, game=game) + except ObjectDoesNotExist: + review = None + + # retrieve other related reviews and marks + if request.user.is_anonymous: + # hide all marks and reviews for anonymous user + mark_list = None + review_list = None + mark_list_more = None + review_list_more = None + else: + mark_list = GameMark.get_available( + game, request.user, request.session['oauth_token']) + review_list = GameReview.get_available( + game, request.user, request.session['oauth_token']) + mark_list_more = True if len(mark_list) > MARK_NUMBER else False + mark_list = mark_list[:MARK_NUMBER] + for m in mark_list: + m.get_status_display = GameMarkStatusTranslator(m.status) + review_list_more = True if len( + review_list) > REVIEW_NUMBER else False + review_list = review_list[:REVIEW_NUMBER] + + # def strip_html_tags(text): + # import re + # regex = re.compile('<.*?>') + # return re.sub(regex, '', text) + + # for r in review_list: + # r.content = strip_html_tags(r.content) + + return render( + request, + 'games/detail.html', + { + 'game': game, + 'mark': mark, + 'review': review, + 'status_enum': MarkStatusEnum, + 'mark_form': mark_form, + 'mark_list': mark_list, + 'mark_list_more': mark_list_more, + 'review_list': review_list, + 'review_list_more': review_list_more, + 'game_tag_list': game_tag_list, + 'mark_tags': mark_tags, + } + ) + else: + logger.warning('non-GET method at /games/') + return HttpResponseBadRequest() + + +@permission_required("games.delete_game") +@login_required +def delete(request, id): + if request.method == 'GET': + game = get_object_or_404(Game, pk=id) + return render( + request, + 'games/delete.html', + { + 'game': game, + } + ) + elif request.method == 'POST': + if request.user.is_staff: + # only staff has right to delete + game = get_object_or_404(Game, pk=id) + game.delete() + return redirect(reverse("common:home")) + else: + raise PermissionDenied() + else: + return HttpResponseBadRequest() + + +# user owned entites +########################### +@mastodon_request_included +@login_required +def create_update_mark(request): + # check list: + # clean rating if is wish + # transaction on updating game rating + # owner check(guarantee) + if request.method == 'POST': + pk = request.POST.get('id') + old_rating = None + old_tags = None + if pk: + mark = get_object_or_404(GameMark, pk=pk) + if request.user != mark.owner: + return HttpResponseBadRequest() + old_rating = mark.rating + old_tags = mark.gamemark_tags.all() + # update + form = GameMarkForm(request.POST, instance=mark) + else: + # create + form = GameMarkForm(request.POST) + + if form.is_valid(): + if form.instance.status == MarkStatusEnum.WISH.value: + form.instance.rating = None + form.cleaned_data['rating'] = None + form.instance.owner = request.user + form.instance.edited_time = timezone.now() + game = form.instance.game + + try: + with transaction.atomic(): + # update game rating + game.update_rating(old_rating, form.instance.rating) + form.save() + # update tags + if old_tags: + for tag in old_tags: + tag.delete() + if form.cleaned_data['tags']: + for tag in form.cleaned_data['tags']: + GameTag.objects.create( + content=tag, + game=game, + mark=form.instance + ) + except IntegrityError as e: + logger.error(e.__str__()) + return HttpResponseServerError("integrity error") + + if form.cleaned_data['share_to_mastodon']: + if form.cleaned_data['is_private']: + visibility = TootVisibilityEnum.PRIVATE + else: + visibility = TootVisibilityEnum.UNLISTED + url = "https://" + request.get_host() + reverse("games:retrieve", + args=[game.id]) + words = GameMarkStatusTranslator(form.cleaned_data['status']) +\ + f"《{game.title}》" + \ + rating_to_emoji(form.cleaned_data['rating']) + + # tags = MASTODON_TAGS % {'category': '书', 'type': '标记'} + tags = '' + content = words + '\n' + url + '\n' + \ + form.cleaned_data['text'] + '\n' + tags + response = post_toot(request.user.mastodon_site, content, visibility, + request.session['oauth_token']) + if response.status_code != 200: + mastodon_logger.error( + f"CODE:{response.status_code} {response.text}") + return HttpResponseServerError("publishing mastodon status failed") + else: + return HttpResponseBadRequest("invalid form data") + + return redirect(reverse("games:retrieve", args=[form.instance.game.id])) + else: + return HttpResponseBadRequest("invalid method") + + +@mastodon_request_included +@login_required +def retrieve_mark_list(request, game_id): + if request.method == 'GET': + game = get_object_or_404(Game, pk=game_id) + queryset = GameMark.get_available( + game, request.user, request.session['oauth_token']) + paginator = Paginator(queryset, MARK_PER_PAGE) + page_number = request.GET.get('page', default=1) + marks = paginator.get_page(page_number) + marks.pagination = PageLinksGenerator( + PAGE_LINK_NUMBER, page_number, paginator.num_pages) + for m in marks: + m.get_status_display = GameMarkStatusTranslator(m.status) + return render( + request, + 'games/mark_list.html', + { + 'marks': marks, + 'game': game, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def delete_mark(request, id): + if request.method == 'POST': + mark = get_object_or_404(GameMark, pk=id) + if request.user != mark.owner: + return HttpResponseBadRequest() + game_id = mark.game.id + try: + with transaction.atomic(): + # update game rating + mark.game.update_rating(mark.rating, None) + mark.delete() + except IntegrityError as e: + return HttpResponseServerError() + return redirect(reverse("games:retrieve", args=[game_id])) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def create_review(request, game_id): + if request.method == 'GET': + form = GameReviewForm(initial={'game': game_id}) + game = get_object_or_404(Game, pk=game_id) + return render( + request, + 'games/create_update_review.html', + { + 'form': form, + 'title': _("添加评论"), + 'game': game, + 'submit_url': reverse("games:create_review", args=[game_id]), + } + ) + elif request.method == 'POST': + form = GameReviewForm(request.POST) + if form.is_valid(): + form.instance.owner = request.user + form.save() + if form.cleaned_data['share_to_mastodon']: + if form.cleaned_data['is_private']: + visibility = TootVisibilityEnum.PRIVATE + else: + visibility = TootVisibilityEnum.UNLISTED + url = "https://" + request.get_host() + reverse("games:retrieve_review", + args=[form.instance.id]) + words = "发布了关于" + f"《{form.instance.game.title}》" + "的评论" + # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'} + tags = '' + content = words + '\n' + url + \ + '\n' + form.cleaned_data['title'] + '\n' + tags + response = post_toot(request.user.mastodon_site, content, visibility, + request.session['oauth_token']) + if response.status_code != 200: + mastodon_logger.error( + f"CODE:{response.status_code} {response.text}") + return HttpResponseServerError("publishing mastodon status failed") + return redirect(reverse("games:retrieve_review", args=[form.instance.id])) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def update_review(request, id): + if request.method == 'GET': + review = get_object_or_404(GameReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + form = GameReviewForm(instance=review) + game = review.game + return render( + request, + 'games/create_update_review.html', + { + 'form': form, + 'title': _("编辑评论"), + 'game': game, + 'submit_url': reverse("games:update_review", args=[review.id]), + } + ) + elif request.method == 'POST': + review = get_object_or_404(GameReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + form = GameReviewForm(request.POST, instance=review) + if form.is_valid(): + form.instance.edited_time = timezone.now() + form.save() + if form.cleaned_data['share_to_mastodon']: + if form.cleaned_data['is_private']: + visibility = TootVisibilityEnum.PRIVATE + else: + visibility = TootVisibilityEnum.UNLISTED + url = "https://" + request.get_host() + reverse("games:retrieve_review", + args=[form.instance.id]) + words = "发布了关于" + f"《{form.instance.game.title}》" + "的评论" + # tags = MASTODON_TAGS % {'category': '书', 'type': '评论'} + tags = '' + content = words + '\n' + url + \ + '\n' + form.cleaned_data['title'] + '\n' + tags + response = post_toot(request.user.mastodon_site, content, visibility, + request.session['oauth_token']) + if response.status_code != 200: + mastodon_logger.error( + f"CODE:{response.status_code} {response.text}") + return HttpResponseServerError("publishing mastodon status failed") + return redirect(reverse("games:retrieve_review", args=[form.instance.id])) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() + + +@login_required +def delete_review(request, id): + if request.method == 'GET': + review = get_object_or_404(GameReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + review_form = GameReviewForm(instance=review) + return render( + request, + 'games/delete_review.html', + { + 'form': review_form, + 'review': review, + } + ) + elif request.method == 'POST': + review = get_object_or_404(GameReview, pk=id) + if request.user != review.owner: + return HttpResponseBadRequest() + game_id = review.game.id + review.delete() + return redirect(reverse("games:retrieve", args=[game_id])) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def retrieve_review(request, id): + if request.method == 'GET': + review = get_object_or_404(GameReview, pk=id) + if not check_visibility(review, request.session['oauth_token'], request.user): + msg = _("你没有访问这个页面的权限😥") + return render( + request, + 'common/error.html', + { + 'msg': msg, + } + ) + review_form = GameReviewForm(instance=review) + game = review.game + try: + mark = GameMark.objects.get(owner=review.owner, game=game) + mark.get_status_display = GameMarkStatusTranslator(mark.status) + except ObjectDoesNotExist: + mark = None + return render( + request, + 'games/review_detail.html', + { + 'form': review_form, + 'review': review, + 'game': game, + 'mark': mark, + } + ) + else: + return HttpResponseBadRequest() + + +@mastodon_request_included +@login_required +def retrieve_review_list(request, game_id): + if request.method == 'GET': + game = get_object_or_404(Game, pk=game_id) + queryset = GameReview.get_available( + game, request.user, request.session['oauth_token']) + paginator = Paginator(queryset, REVIEW_PER_PAGE) + page_number = request.GET.get('page', default=1) + reviews = paginator.get_page(page_number) + reviews.pagination = PageLinksGenerator( + PAGE_LINK_NUMBER, page_number, paginator.num_pages) + return render( + request, + 'games/review_list.html', + { + 'reviews': reviews, + 'game': game, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def scrape(request): + if request.method == 'GET': + keywords = request.GET.get('q') + form = GameForm() + return render( + request, + 'games/scrape.html', + { + 'q': keywords, + 'form': form, + } + ) + else: + return HttpResponseBadRequest() + + +@login_required +def click_to_scrape(request): + if request.method == "POST": + url = request.POST.get("url") + if url: + return jump_or_scrape(request, url) + else: + return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() diff --git a/movies/templates/movies/create_update_review.html b/movies/templates/movies/create_update_review.html index 223d30a0..1f4e5b34 100644 --- a/movies/templates/movies/create_update_review.html +++ b/movies/templates/movies/create_update_review.html @@ -49,7 +49,7 @@ {% if movie.year %}({{ movie.year }}){% endif %} {% endif %} - {{ movie.get_source_site_display }} + {{ movie.get_source_site_display }}
    {% if movie.director %}{% trans '导演:' %} {% for director in movie.director %} diff --git a/movies/templates/movies/detail.html b/movies/templates/movies/detail.html index 58146c50..8dbb4a09 100644 --- a/movies/templates/movies/detail.html +++ b/movies/templates/movies/detail.html @@ -190,7 +190,7 @@ {% if movie.other_info %} {% for k, v in movie.other_info.items %}
    - {{k}}:{{v}} + {{ k }}:{{ v | urlize }}
    {% endfor %} {% endif %} diff --git a/movies/templates/movies/scrape.html b/movies/templates/movies/scrape.html index e2cd4730..91cd90c9 100644 --- a/movies/templates/movies/scrape.html +++ b/movies/templates/movies/scrape.html @@ -44,7 +44,7 @@
    {% trans '根据豆瓣内容填写下方表单' %}
    - +
    diff --git a/music/templates/music/album_detail.html b/music/templates/music/album_detail.html index 7f98941b..68fe1143 100644 --- a/music/templates/music/album_detail.html +++ b/music/templates/music/album_detail.html @@ -119,7 +119,7 @@ {% if album.other_info %} {% for k, v in album.other_info.items %}
    - {{k}}:{{v}} + {{ k }}:{{ v | urlize }}
    {% endfor %} {% endif %} diff --git a/music/templates/music/create_update_album_review.html b/music/templates/music/create_update_album_review.html index 2166cc9e..db1f11f8 100644 --- a/music/templates/music/create_update_album_review.html +++ b/music/templates/music/create_update_album_review.html @@ -40,7 +40,7 @@
    {{ album.title }} - {{ album.get_source_site_display }} + {{ album.get_source_site_display }}
    {% if album.artist %}{% trans '艺术家:' %} {% for artist in album.artist %} diff --git a/music/templates/music/create_update_song_review.html b/music/templates/music/create_update_song_review.html index 695bfeff..03e8271f 100644 --- a/music/templates/music/create_update_song_review.html +++ b/music/templates/music/create_update_song_review.html @@ -40,7 +40,7 @@
    {{ song.title }} - {{ song.get_source_site_display }} + {{ song.get_source_site_display }}
    {% if song.artist %}{% trans '艺术家:' %} {% for artist in song.artist %} diff --git a/music/templates/music/scrape_album.html b/music/templates/music/scrape_album.html index 01d48925..8079aa9e 100644 --- a/music/templates/music/scrape_album.html +++ b/music/templates/music/scrape_album.html @@ -44,7 +44,7 @@
    {% trans '根据豆瓣内容填写下方表单' %}
    - +
    @@ -54,14 +54,17 @@ {% for field in form %} - {% if field.id_for_label == 'id_is_series' %} - - {{ field }} + {% if field.name == 'release_date' %} + {{ field.label_tag }} + + + {% else %} - {% if field.id_for_label != 'id_id' %} - - {% endif %} - {{ field }} + {% if field.name != 'id' %} + {{ field.label_tag }} + {% endif %} + {{ field }} {% endif %} {% endfor %} diff --git a/music/templates/music/song_detail.html b/music/templates/music/song_detail.html index 59ec6478..6aa9b87b 100644 --- a/music/templates/music/song_detail.html +++ b/music/templates/music/song_detail.html @@ -107,7 +107,7 @@ {% if song.other_info %} {% for k, v in song.other_info.items %}
    - {{k}}:{{v}} + {{ k }}:{{ v | urlize }}
    {% endfor %} {% endif %} diff --git a/users/templates/users/book_list.html b/users/templates/users/book_list.html index 80f8d08e..ff2dfea9 100644 --- a/users/templates/users/book_list.html +++ b/users/templates/users/book_list.html @@ -95,7 +95,7 @@ {% endif %}

    - {{ mark.book.brief | truncate:170 }} + {{ mark.book.brief }}

    {% for tag_dict in mark.book.tag_list %} diff --git a/users/templates/users/game_list.html b/users/templates/users/game_list.html new file mode 100644 index 00000000..f4e97145 --- /dev/null +++ b/users/templates/users/game_list.html @@ -0,0 +1,272 @@ +{% load static %} +{% load i18n %} +{% load l10n %} +{% load humanize %} +{% load admin_url %} +{% load mastodon %} +{% load oauth_token %} +{% load truncate %} +{% load thumb %} + + + + + + + {% trans 'NiceDB - ' %}{{ user.username }}{{ list_title }} + + + + + + + + + + +
    +
    + {% include "partial/_navbar.html" %} + +
    +
    +
    +
    +
    + +
    +
    + {{ user.username }}{{ list_title }} +
    +
    +
      + + {% for mark in marks %} + +
    • +
      + + + +
      +
      + + + {% if mark.game.other_title %}{% trans '别名' %} + {% for other_title in mark.game.other_title %} + {{ other_title }}{% if not forloop.last %} {% endif %} + {% endfor %}/ + {% endif %} + + {% if mark.game.developer %}{% trans '开发商' %} + {% for developer in mark.game.developer %} + {{ developer }}{% if not forloop.last %} {% endif %} + {% endfor %}/ + {% endif %} + + {% if mark.game.genre %}{% trans '类型' %} + {% for genre in mark.game.genre %} + {{ genre }}{% if not forloop.last %} {% endif %} + {% endfor %}/ + {% endif %} + + {% if mark.game.platform %}{% trans '平台' %} + {% for platform in mark.game.platform %} + {{ platform }}{% if not forloop.last %} {% endif %} + {% endfor %}/ + {% endif %} + + +

      + {{ mark.game.brief }} +

      +
      + {% for tag_dict in mark.game.tag_list %} + {% for k, v in tag_dict.items %} + {% if k == 'content' %} + + {{ v }} + + {% endif %} + {% endfor %} + {% endfor %} +
      +
      +
      +
      +
        +
      • + + {% if mark.rating %} + + {% endif %} + {% if mark.is_private %} + + + + {% endif %} + {% trans '于' %} {{ mark.edited_time }} {% trans '标记' %} + {% if mark.text %} +

        {{ mark.text }}

        + {% endif %} +
      • +
      +
      +
      + +
    • + {% empty %} +
      {% trans '无结果' %}
      + {% endfor %} + + + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    + + + + + +
    +
    +
    + +
    +
    + {% trans '关注的人' %} +
    + {% trans '更多' %} + +
    + +
    +
    + {% trans '被他们关注' %} +
    + {% trans '更多' %} + +
    + +
    +
    +
    + +
    +
    +
    +
    + {% include "partial/_footer.html" %} +
    + + + + + + {% if user == request.user %} + + {% else %} + + {% endif %} + + + + + + + diff --git a/common/templates/common/home.html b/users/templates/users/home.html similarity index 86% rename from common/templates/common/home.html rename to users/templates/users/home.html index 55cf515d..ea3fbecc 100644 --- a/common/templates/common/home.html +++ b/users/templates/users/home.html @@ -40,7 +40,7 @@
    {% trans '想读的书' %}
    - {% if wish_books_more %} + {% if wish_book_more %} {% trans '更多' %} {% endif %} @@ -65,7 +65,7 @@
    {% trans '在读的书' %}
    - {% if do_books_more %} + {% if do_book_more %} {% trans '更多' %} {% endif %} @@ -91,7 +91,7 @@
    {% trans '读过的书' %}
    - {% if collect_books_more %} + {% if collect_book_more %} {% trans '更多' %} {% endif %} @@ -117,7 +117,7 @@
    {% trans '想看的电影/剧集' %}
    - {% if wish_movies_more %} + {% if wish_movie_more %} {% trans '更多' %} {% endif %} @@ -143,7 +143,7 @@
    {% trans '在看的电影/剧集' %}
    - {% if do_movies_more %} + {% if do_movie_more %} {% trans '更多' %} {% endif %} @@ -169,7 +169,7 @@
    {% trans '看过的电影/剧集' %}
    - {% if collect_movies_more %} + {% if collect_movie_more %} {% trans '更多' %} {% endif %} @@ -299,6 +299,81 @@
    +
    +
    + {% trans '想玩的游戏' %} +
    + {% if wish_game_more %} + {% trans '更多' %} + {% endif %} + + +
    + +
    +
    + {% trans '在玩的游戏' %} +
    + {% if do_game_more %} + {% trans '更多' %} + {% endif %} + + +
    + +
    +
    + {% trans '玩过的游戏' %} +
    + {% if collect_game_more %} + {% trans '更多' %} + {% endif %} + + +
    + +
    {% if user == request.user %} diff --git a/users/templates/users/movie_list.html b/users/templates/users/movie_list.html index 93c54cc8..fa21d29a 100644 --- a/users/templates/users/movie_list.html +++ b/users/templates/users/movie_list.html @@ -98,7 +98,7 @@ {% endif %}

    - {{ mark.movie.brief | truncate:170 }} + {{ mark.movie.brief }}

    {% for tag_dict in mark.movie.tag_list %} diff --git a/users/urls.py b/users/urls.py index d7cf7977..121c1afe 100644 --- a/users/urls.py +++ b/users/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ path('/book//', book_list, name='book_list'), path('/movie//', movie_list, name='movie_list'), path('/music//', music_list, name='music_list'), + path('/game//', game_list, name='game_list'), path('/', home, name='home'), path('/followers/', followers, name='followers'), path('/following/', following, name='following'), diff --git a/users/views.py b/users/views.py index a29970a8..8d751792 100644 --- a/users/views.py +++ b/users/views.py @@ -12,15 +12,18 @@ from .forms import ReportForm from mastodon.auth import * from mastodon.api import * from mastodon import mastodon_request_included -from common.views import BOOKS_PER_SET, ITEMS_PER_PAGE, PAGE_LINK_NUMBER, TAG_NUMBER_ON_LIST, MOVIES_PER_SET, MUSIC_PER_SET +from common.config import * from common.models import MarkStatusEnum from common.utils import PageLinksGenerator +from management.models import Announcement from books.models import * from movies.models import * from music.models import * +from games.models import * from books.forms import BookMarkStatusTranslator from movies.forms import MovieMarkStatusTranslator from music.forms import MusicMarkStatusTranslator +from games.forms import GameMarkStatusTranslator from mastodon.models import MastodonApplication @@ -172,10 +175,37 @@ def home(request, id): 'secondary_msg': sec_msg, } ) + + # access one's own home page if user == request.user: - return redirect("common:home") + reports = Report.objects.order_by( + '-submitted_time').filter(is_read=False) + unread_announcements = Announcement.objects.filter( + pk__gt=request.user.read_announcement_index).order_by('-pk') + try: + request.user.read_announcement_index = Announcement.objects.latest( + 'pk').pk + request.user.save(update_fields=['read_announcement_index']) + except ObjectDoesNotExist as e: + # when there is no annoucenment + pass + book_marks = request.user.user_bookmarks.all() + movie_marks = request.user.user_moviemarks.all() + album_marks = request.user.user_albummarks.all() + song_marks = request.user.user_songmarks.all() + game_marks = request.user.user_gamemarks.all() + + # visit other's home page else: - # mastodon request + # no these value on other's home page + reports = None + unread_announcements = None + + # cross site info for visiting other's home page + user.target_site_id = get_cross_site_id( + user, request.user.mastodon_site, request.session['oauth_token']) + + # make queries relation = get_relationship(request.user, user, request.session['oauth_token'])[0] if relation['blocked_by']: msg = _("你没有访问TA主页的权限😥") @@ -187,89 +217,86 @@ def home(request, id): } ) book_marks = BookMark.get_available_by_user(user, relation['following']) - do_book_marks = book_marks.filter(status=MarkStatusEnum.DO) - do_books_more = True if do_book_marks.count() > BOOKS_PER_SET else False - - wish_book_marks = book_marks.filter(status=MarkStatusEnum.WISH) - wish_books_more = True if wish_book_marks.count() > BOOKS_PER_SET else False - - collect_book_marks = book_marks.filter(status=MarkStatusEnum.COLLECT) - collect_books_more = True if collect_book_marks.count() > BOOKS_PER_SET else False - movie_marks = MovieMark.get_available_by_user(user, relation['following']) - do_movie_marks = movie_marks.filter(status=MarkStatusEnum.DO) - do_movies_more = True if do_movie_marks.count() > BOOKS_PER_SET else False - - wish_movie_marks = movie_marks.filter(status=MarkStatusEnum.WISH) - wish_movies_more = True if wish_movie_marks.count() > BOOKS_PER_SET else False - - collect_movie_marks = movie_marks.filter(status=MarkStatusEnum.COLLECT) - collect_movies_more = True if collect_movie_marks.count() > BOOKS_PER_SET else False - song_marks = SongMark.get_available_by_user(user, relation['following']) album_marks = AlbumMark.get_available_by_user(user, relation['following']) + game_marks = GameMark.get_available_by_user(user, relation['following']) - do_music_marks = list(song_marks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) \ - + list(album_marks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) - do_music_more = True if len(do_music_marks) > MUSIC_PER_SET else False - do_music_marks = sorted(do_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] + # book marks + filtered_book_marks = filter_marks(book_marks, BOOKS_PER_SET, 'book') - wish_music_marks = list(song_marks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) \ - + list(album_marks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) - wish_music_more = True if len(wish_music_marks) > MUSIC_PER_SET else False - wish_music_marks = sorted(wish_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] + # movie marks + filtered_movie_marks = filter_marks(movie_marks, MOVIES_PER_SET, 'movie') - collect_music_marks = list(song_marks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) \ - + list(album_marks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) - collect_music_more = True if len(collect_music_marks) > MUSIC_PER_SET else False - collect_music_marks = sorted(collect_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] + # game marks + filtered_game_marks = filter_marks(game_marks, GAMES_PER_SET, 'game') - for mark in do_music_marks + wish_music_marks + collect_music_marks: - # for template convenience - if mark.__class__ == AlbumMark: - mark.type = "album" - else: - mark.type = "song" + # music marks + do_music_marks = list(song_marks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) \ + + list(album_marks.filter(status=MarkStatusEnum.DO)[:MUSIC_PER_SET]) + do_music_more = True if len(do_music_marks) > MUSIC_PER_SET else False + do_music_marks = sorted(do_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] - user.target_site_id = get_cross_site_id( - user, request.user.mastodon_site, request.session['oauth_token']) + wish_music_marks = list(song_marks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) \ + + list(album_marks.filter(status=MarkStatusEnum.WISH)[:MUSIC_PER_SET]) + wish_music_more = True if len(wish_music_marks) > MUSIC_PER_SET else False + wish_music_marks = sorted(wish_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] - try: - layout = user.preference.get_serialized_home_layout() - except ObjectDoesNotExist: - Preference.objects.create(user=user) - layout = user.preference.get_serialized_home_layout() + collect_music_marks = list(song_marks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) \ + + list(album_marks.filter(status=MarkStatusEnum.COLLECT)[:MUSIC_PER_SET]) + collect_music_more = True if len(collect_music_marks) > MUSIC_PER_SET else False + collect_music_marks = sorted(collect_music_marks, key=lambda e: e.edited_time, reverse=True)[:MUSIC_PER_SET] - return render( - request, - 'common/home.html', - { - 'user': user, - 'do_book_marks': do_book_marks[:BOOKS_PER_SET], - 'wish_book_marks': wish_book_marks[:BOOKS_PER_SET], - 'collect_book_marks': collect_book_marks[:BOOKS_PER_SET], - 'do_books_more': do_books_more, - 'wish_books_more': wish_books_more, - 'collect_books_more': collect_books_more, - 'do_movie_marks': do_movie_marks[:MOVIES_PER_SET], - 'wish_movie_marks': wish_movie_marks[:MOVIES_PER_SET], - 'collect_movie_marks': collect_movie_marks[:MOVIES_PER_SET], - 'do_movies_more': do_movies_more, - 'wish_movies_more': wish_movies_more, - 'collect_movies_more': collect_movies_more, - 'do_music_marks': do_music_marks, - 'wish_music_marks': wish_music_marks, - 'collect_music_marks': collect_music_marks, - 'do_music_more': do_music_more, - 'wish_music_more': wish_music_more, - 'collect_music_more': collect_music_more, - 'layout': layout, - } - ) + for mark in do_music_marks + wish_music_marks + collect_music_marks: + # for template convenience + if mark.__class__ == AlbumMark: + mark.type = "album" + else: + mark.type = "song" + + try: + layout = user.preference.get_serialized_home_layout() + except ObjectDoesNotExist: + Preference.objects.create(user=user) + layout = user.preference.get_serialized_home_layout() + + return render( + request, + 'users/home.html', + { + 'user': user, + **filtered_book_marks, + **filtered_movie_marks, + **filtered_game_marks, + 'do_music_marks': do_music_marks, + 'wish_music_marks': wish_music_marks, + 'collect_music_marks': collect_music_marks, + 'do_music_more': do_music_more, + 'wish_music_more': wish_music_more, + 'collect_music_more': collect_music_more, + 'layout': layout, + 'reports': reports, + 'unread_announcements': unread_announcements, + } + ) else: return HttpResponseBadRequest() +def filter_marks(queryset, maximum, type_name): + result = {} + for k in MarkStatusEnum.names: + result[f"{k}_{type_name}_marks"] = queryset.filter( + status=MarkStatusEnum[k.upper()] + ).order_by("-edited_time") + if result[f"{k}_{type_name}_marks"].count() > maximum: + result[f"{k}_{type_name}_more"] = True + result[f"{k}_{type_name}_marks"] = result[f"{k}_{type_name}_marks"][:maximum] + else: + result[f"{k}_{type_name}_more"] = False + return result + + @mastodon_request_included @login_required def followers(request, id): @@ -429,7 +456,7 @@ def book_list(request, id, status): mark.book.tag_list = mark.book.get_tags_manager().values('content').annotate( tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST] marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages) - list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的书")) + list_title = str(BookMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的书")) return render( request, 'users/book_list.html', @@ -471,7 +498,7 @@ def movie_list(request, id, status): 'msg': msg, 'secondary_msg': sec_msg, } - ) + ) if not user == request.user: # mastodon request relation = get_relationship(request.user, user, request.session['oauth_token'])[0] @@ -484,10 +511,11 @@ def movie_list(request, id, status): 'msg': msg, } ) - queryset = MovieMark.get_available_by_user(user, relation['following']).filter( - status=MarkStatusEnum[status.upper()]).order_by("-edited_time") user.target_site_id = get_cross_site_id( user, request.user.mastodon_site, request.session['oauth_token']) + + queryset = MovieMark.get_available_by_user(user, relation['following']).filter( + status=MarkStatusEnum[status.upper()]).order_by("-edited_time") else: queryset = MovieMark.objects.filter( owner=user, status=MarkStatusEnum[status.upper()]).order_by("-edited_time") @@ -498,7 +526,7 @@ def movie_list(request, id, status): mark.movie.tag_list = mark.movie.get_tags_manager().values('content').annotate( tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST] marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages) - list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的电影和剧集")) + list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的电影和剧集")) return render( request, 'users/movie_list.html', @@ -512,6 +540,76 @@ def movie_list(request, id, status): return HttpResponseBadRequest() +@mastodon_request_included +@login_required +def game_list(request, id, status): + if request.method == 'GET': + if not status.upper() in MarkStatusEnum.names: + return HttpResponseBadRequest() + + if isinstance(id, str): + try: + username = id.split('@')[0] + site = id.split('@')[1] + except IndexError as e: + return HttpResponseBadRequest("Invalid user id") + query_kwargs = {'username': username, 'mastodon_site': site} + elif isinstance(id, int): + query_kwargs = {'pk': id} + try: + user = User.objects.get(**query_kwargs) + except ObjectDoesNotExist: + msg = _("😖哎呀这位老师还没有注册书影音呢,快去长毛象喊TA来吧!") + sec_msg = _("目前只开放本站用户注册") + return render( + request, + 'common/error.html', + { + 'msg': msg, + 'secondary_msg': sec_msg, + } + ) + if not user == request.user: + # mastodon request + relation = get_relationship(request.user, user, request.session['oauth_token'])[0] + if relation['blocked_by']: + msg = _("你没有访问TA主页的权限😥") + return render( + request, + 'common/error.html', + { + 'msg': msg, + } + ) + user.target_site_id = get_cross_site_id( + user, request.user.mastodon_site, request.session['oauth_token']) + + queryset = GameMark.get_available_by_user(user, relation['following']).filter( + status=MarkStatusEnum[status.upper()]).order_by("-edited_time") + else: + queryset = GameMark.objects.filter( + owner=user, status=MarkStatusEnum[status.upper()]).order_by("-edited_time") + paginator = Paginator(queryset, ITEMS_PER_PAGE) + page_number = request.GET.get('page', default=1) + marks = paginator.get_page(page_number) + for mark in marks: + mark.game.tag_list = mark.game.get_tags_manager().values('content').annotate( + tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST] + marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages) + list_title = str(GameMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的游戏")) + return render( + request, + 'users/game_list.html', + { + 'marks': marks, + 'user': user, + 'list_title' : list_title, + } + ) + else: + return HttpResponseBadRequest() + + @mastodon_request_included @login_required def music_list(request, id, status): @@ -578,7 +676,7 @@ def music_list(request, id, status): tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST] marks.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages) - list_title = str(MovieMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("标记的音乐")) + list_title = str(MusicMarkStatusTranslator(MarkStatusEnum[status.upper()])) + str(_("的音乐")) return render( request, 'users/music_list.html',