mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-17 06:27:28 +00:00
add pdf thumbnail (#6157)
Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
This commit is contained in:
parent
0b84b22df9
commit
ca83ef90dc
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import cookie from 'react-cookies';
|
import cookie from 'react-cookies';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { navigate } from '@gatsbyjs/reach-router';
|
import { navigate } from '@gatsbyjs/reach-router';
|
||||||
import { gettext, siteRoot, username, enableVideoThumbnail } from '../../utils/constants';
|
import { gettext, siteRoot, username, enableVideoThumbnail, enablePDFThumbnail } from '../../utils/constants';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import collabServer from '../../utils/collab-server';
|
import collabServer from '../../utils/collab-server';
|
||||||
@ -538,7 +538,7 @@ class LibContentView extends React.Component {
|
|||||||
|
|
||||||
getThumbnails = (repoID, path, direntList) => {
|
getThumbnails = (repoID, path, direntList) => {
|
||||||
let items = direntList.filter((item) => {
|
let items = direntList.filter((item) => {
|
||||||
return (Utils.imageCheck(item.name) || (enableVideoThumbnail && Utils.videoCheck(item.name))) && !item.encoded_thumbnail_src;
|
return (Utils.imageCheck(item.name) || (enableVideoThumbnail && Utils.videoCheck(item.name)) || (enablePDFThumbnail && Utils.pdfCheck(item.name))) && !item.encoded_thumbnail_src;
|
||||||
});
|
});
|
||||||
if (items.length == 0) {
|
if (items.length == 0) {
|
||||||
return ;
|
return ;
|
||||||
|
@ -5,7 +5,7 @@ import { Link, navigate } from '@gatsbyjs/reach-router';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import { gettext, siteRoot, enableVideoThumbnail } from '../../utils/constants';
|
import { gettext, siteRoot, enableVideoThumbnail, enablePDFThumbnail } from '../../utils/constants';
|
||||||
import EmptyTip from '../../components/empty-tip';
|
import EmptyTip from '../../components/empty-tip';
|
||||||
import Loading from '../../components/loading';
|
import Loading from '../../components/loading';
|
||||||
import toaster from '../../components/toast';
|
import toaster from '../../components/toast';
|
||||||
@ -79,7 +79,10 @@ class TableBody extends Component {
|
|||||||
|
|
||||||
getThumbnails() {
|
getThumbnails() {
|
||||||
let items = this.state.items.filter((item) => {
|
let items = this.state.items.filter((item) => {
|
||||||
return (Utils.imageCheck(item.obj_name) || (enableVideoThumbnail && Utils.videoCheck(item.obj_name))) && !item.repo_encrypted && !item.encoded_thumbnail_src && !item.deleted;
|
return (Utils.imageCheck(item.obj_name) ||
|
||||||
|
(enableVideoThumbnail && Utils.videoCheck(item.obj_name)) ||
|
||||||
|
(enablePDFThumbnail && Utils.pdfCheck(item.obj_name))) &&
|
||||||
|
!item.repo_encrypted && !item.encoded_thumbnail_src && !item.deleted;
|
||||||
});
|
});
|
||||||
if (items.length == 0) {
|
if (items.length == 0) {
|
||||||
return ;
|
return ;
|
||||||
|
@ -30,7 +30,7 @@ let {
|
|||||||
repoID, relativePath,
|
repoID, relativePath,
|
||||||
mode, thumbnailSize, zipped,
|
mode, thumbnailSize, zipped,
|
||||||
trafficOverLimit, canDownload,
|
trafficOverLimit, canDownload,
|
||||||
noQuota, canUpload, enableVideoThumbnail
|
noQuota, canUpload, enableVideoThumbnail, enablePDFThumbnail
|
||||||
} = window.shared.pageOptions;
|
} = window.shared.pageOptions;
|
||||||
|
|
||||||
const showDownloadIcon = !trafficOverLimit && canDownload;
|
const showDownloadIcon = !trafficOverLimit && canDownload;
|
||||||
@ -111,7 +111,8 @@ class SharedDirView extends React.Component {
|
|||||||
let items = this.state.items.filter((item) => {
|
let items = this.state.items.filter((item) => {
|
||||||
return !item.is_dir &&
|
return !item.is_dir &&
|
||||||
(Utils.imageCheck(item.file_name) ||
|
(Utils.imageCheck(item.file_name) ||
|
||||||
(enableVideoThumbnail && Utils.videoCheck(item.file_name))) &&
|
(enableVideoThumbnail && Utils.videoCheck(item.file_name)) ||
|
||||||
|
(enablePDFThumbnail && Utils.pdfCheck(item.file_name))) &&
|
||||||
!item.encoded_thumbnail_src;
|
!item.encoded_thumbnail_src;
|
||||||
});
|
});
|
||||||
if (items.length == 0) {
|
if (items.length == 0) {
|
||||||
|
@ -91,6 +91,7 @@ export const curNoteID = window.app.pageOptions.curNoteID;
|
|||||||
export const enableTC = window.app.pageOptions.enableTC;
|
export const enableTC = window.app.pageOptions.enableTC;
|
||||||
|
|
||||||
export const enableVideoThumbnail = window.app.pageOptions.enableVideoThumbnail;
|
export const enableVideoThumbnail = window.app.pageOptions.enableVideoThumbnail;
|
||||||
|
export const enablePDFThumbnail = window.app.pageOptions.enablePDFThumbnail;
|
||||||
|
|
||||||
export const enableOnlyoffice = window.app.pageOptions.enableOnlyoffice || false;
|
export const enableOnlyoffice = window.app.pageOptions.enableOnlyoffice || false;
|
||||||
export const onlyofficeConverterExtensions = window.app.pageOptions.onlyofficeConverterExtensions || [];
|
export const onlyofficeConverterExtensions = window.app.pageOptions.onlyofficeConverterExtensions || [];
|
||||||
|
@ -124,6 +124,19 @@ export const Utils = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pdfCheck: function(filename) {
|
||||||
|
if (filename.lastIndexOf('.') == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var file_ext = filename.substr(filename.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
var image_exts = ['pdf'];
|
||||||
|
if (image_exts.indexOf(file_ext) != -1) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
getShareLinkPermissionList: function(itemType, permission, path, canEdit) {
|
getShareLinkPermissionList: function(itemType, permission, path, canEdit) {
|
||||||
// itemType: library, dir, file
|
// itemType: library, dir, file
|
||||||
// permission: rw, r, admin, cloud-edit, preview, custom-*
|
// permission: rw, r, admin, cloud-edit, preview, custom-*
|
||||||
|
@ -26,3 +26,4 @@ Markdown==3.4.*
|
|||||||
bleach==5.0.*
|
bleach==5.0.*
|
||||||
python-ldap==3.4.*
|
python-ldap==3.4.*
|
||||||
pypinyin==0.50.*
|
pypinyin==0.50.*
|
||||||
|
PyMuPDF==1.24.*
|
||||||
|
@ -23,7 +23,7 @@ from seahub.utils import check_filename_with_rename, is_valid_dirent_name, \
|
|||||||
normalize_dir_path, is_pro_version, FILEEXT_TYPE_MAP, get_file_type_and_ext
|
normalize_dir_path, is_pro_version, FILEEXT_TYPE_MAP, get_file_type_and_ext
|
||||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||||
from seahub.utils.file_tags import get_files_tags_in_dir
|
from seahub.utils.file_tags import get_files_tags_in_dir
|
||||||
from seahub.utils.file_types import IMAGE, VIDEO, XMIND, SEADOC
|
from seahub.utils.file_types import IMAGE, VIDEO, XMIND, SEADOC, PDF
|
||||||
from seahub.base.models import UserStarredFiles
|
from seahub.base.models import UserStarredFiles
|
||||||
from seahub.base.templatetags.seahub_tags import email2nickname, \
|
from seahub.base.templatetags.seahub_tags import email2nickname, \
|
||||||
email2contact_email
|
email2contact_email
|
||||||
@ -175,7 +175,7 @@ def get_dir_file_info_list(username, request_type, repo_obj, parent_dir,
|
|||||||
fileExt = os.path.splitext(file_name)[1][1:].lower()
|
fileExt = os.path.splitext(file_name)[1][1:].lower()
|
||||||
file_type = FILEEXT_TYPE_MAP.get(fileExt)
|
file_type = FILEEXT_TYPE_MAP.get(fileExt)
|
||||||
|
|
||||||
if file_type in (IMAGE, XMIND) or \
|
if file_type in (IMAGE, XMIND, PDF) or \
|
||||||
(file_type == VIDEO and ENABLE_VIDEO_THUMBNAIL):
|
(file_type == VIDEO and ENABLE_VIDEO_THUMBNAIL):
|
||||||
|
|
||||||
# if thumbnail has already been created, return its src.
|
# if thumbnail has already been created, return its src.
|
||||||
|
@ -747,6 +747,9 @@ THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT = 256
|
|||||||
ENABLE_VIDEO_THUMBNAIL = False
|
ENABLE_VIDEO_THUMBNAIL = False
|
||||||
THUMBNAIL_VIDEO_FRAME_TIME = 5 # use the frame at 5 second as thumbnail
|
THUMBNAIL_VIDEO_FRAME_TIME = 5 # use the frame at 5 second as thumbnail
|
||||||
|
|
||||||
|
# pdf thumbnails
|
||||||
|
ENABLE_PDF_THUMBNAIL = True
|
||||||
|
|
||||||
# template for create new office file
|
# template for create new office file
|
||||||
OFFICE_TEMPLATE_ROOT = os.path.join(MEDIA_ROOT, 'office-template')
|
OFFICE_TEMPLATE_ROOT = os.path.join(MEDIA_ROOT, 'office-template')
|
||||||
|
|
||||||
|
@ -141,6 +141,7 @@
|
|||||||
enableTC: {% if enable_terms_and_conditions %} true {% else %} false {% endif %},
|
enableTC: {% if enable_terms_and_conditions %} true {% else %} false {% endif %},
|
||||||
enableSSOToThirdpartWebsite: {% if enable_sso_to_thirdpart_website %} true {% else %} false {% endif %},
|
enableSSOToThirdpartWebsite: {% if enable_sso_to_thirdpart_website %} true {% else %} false {% endif %},
|
||||||
enableVideoThumbnail: {% if enable_video_thumbnail %} true {% else %} false {% endif %},
|
enableVideoThumbnail: {% if enable_video_thumbnail %} true {% else %} false {% endif %},
|
||||||
|
enablePDFThumbnail: {% if enable_pdf_thumbnail %} true {% else %} false {% endif %},
|
||||||
showLogoutIcon: {% if show_logout_icon %} true {% else %} false {% endif %},
|
showLogoutIcon: {% if show_logout_icon %} true {% else %} false {% endif %},
|
||||||
additionalShareDialogNote: {% if additional_share_dialog_note %} {{ additional_share_dialog_note|safe }} {% else %} null {% endif %},
|
additionalShareDialogNote: {% if additional_share_dialog_note %} {{ additional_share_dialog_note|safe }} {% else %} null {% endif %},
|
||||||
additionalAppBottomLinks: {% if additional_app_bottom_links %} {{ additional_app_bottom_links|safe }} {% else %} null {% endif %},
|
additionalAppBottomLinks: {% if additional_app_bottom_links %} {{ additional_app_bottom_links|safe }} {% else %} null {% endif %},
|
||||||
|
@ -24,6 +24,10 @@ thumbnailSizeForOriginal: {{ thumbnail_size_for_original }},
|
|||||||
enableVideoThumbnail: {% if enable_video_thumbnail %} true {% else %} false {% endif %},
|
enableVideoThumbnail: {% if enable_video_thumbnail %} true {% else %} false {% endif %},
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if filetype == 'PDF' %}
|
||||||
|
enablePDFThumbnail: {% if enable_pdf_thumbnail %} true {% else %} false {% endif %},
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if filetype == 'XMind' %}
|
{% if filetype == 'XMind' %}
|
||||||
xmindImageSrc: '{{ xmind_image_src|escapejs }}',
|
xmindImageSrc: '{{ xmind_image_src|escapejs }}',
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
mode: '{{ mode }}',
|
mode: '{{ mode }}',
|
||||||
thumbnailSize: {{ thumbnail_size }},
|
thumbnailSize: {{ thumbnail_size }},
|
||||||
enableVideoThumbnail: {% if enable_video_thumbnail %}true{% else %}false{% endif %},
|
enableVideoThumbnail: {% if enable_video_thumbnail %}true{% else %}false{% endif %},
|
||||||
|
enablePDFThumbnail: {% if enable_pdf_thumbnail %}true{% else %}false{% endif %},
|
||||||
trafficOverLimit: {% if traffic_over_limit %}true{% else %}false{% endif %},
|
trafficOverLimit: {% if traffic_over_limit %}true{% else %}false{% endif %},
|
||||||
canDownload: {% if permissions.can_download %}true{% else %}false{% endif %},
|
canDownload: {% if permissions.can_download %}true{% else %}false{% endif %},
|
||||||
noQuota: {% if no_quota %}true{% else %}false{% endif %},
|
noQuota: {% if no_quota %}true{% else %}false{% endif %},
|
||||||
|
@ -8,6 +8,7 @@ import logging
|
|||||||
import subprocess
|
import subprocess
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from fitz import open as fitz_open
|
||||||
try: # Py2 and Py3 compatibility
|
try: # Py2 and Py3 compatibility
|
||||||
from urllib.request import urlretrieve
|
from urllib.request import urlretrieve
|
||||||
except:
|
except:
|
||||||
@ -18,7 +19,7 @@ from seaserv import get_file_id_by_path, get_repo, get_file_size, \
|
|||||||
seafile_api
|
seafile_api
|
||||||
|
|
||||||
from seahub.utils import gen_inner_file_get_url, get_file_type_and_ext
|
from seahub.utils import gen_inner_file_get_url, get_file_type_and_ext
|
||||||
from seahub.utils.file_types import VIDEO, XMIND
|
from seahub.utils.file_types import VIDEO, XMIND, PDF
|
||||||
from seahub.settings import THUMBNAIL_IMAGE_SIZE_LIMIT, \
|
from seahub.settings import THUMBNAIL_IMAGE_SIZE_LIMIT, \
|
||||||
THUMBNAIL_EXTENSION, THUMBNAIL_ROOT, THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT,\
|
THUMBNAIL_EXTENSION, THUMBNAIL_ROOT, THUMBNAIL_IMAGE_ORIGINAL_SIZE_LIMIT,\
|
||||||
ENABLE_VIDEO_THUMBNAIL, THUMBNAIL_VIDEO_FRAME_TIME
|
ENABLE_VIDEO_THUMBNAIL, THUMBNAIL_VIDEO_FRAME_TIME
|
||||||
@ -117,7 +118,10 @@ def generate_thumbnail(request, repo_id, size, path):
|
|||||||
thumbnail_file, file_size)
|
thumbnail_file, file_size)
|
||||||
else:
|
else:
|
||||||
return (False, 400)
|
return (False, 400)
|
||||||
|
if filetype == PDF:
|
||||||
|
# pdf thumbnails
|
||||||
|
return create_pdf_thumbnails(repo, file_id, path, size,
|
||||||
|
thumbnail_file, file_size)
|
||||||
if filetype == XMIND:
|
if filetype == XMIND:
|
||||||
return extract_xmind_image(repo_id, path, size)
|
return extract_xmind_image(repo_id, path, size)
|
||||||
|
|
||||||
@ -181,6 +185,41 @@ def create_psd_thumbnails(repo, file_id, path, size, thumbnail_file, file_size):
|
|||||||
os.unlink(tmp_img_path)
|
os.unlink(tmp_img_path)
|
||||||
return (False, 500)
|
return (False, 500)
|
||||||
|
|
||||||
|
|
||||||
|
def create_pdf_thumbnails(repo, file_id, path, size, thumbnail_file, file_size):
|
||||||
|
t1 = timeit.default_timer()
|
||||||
|
token = seafile_api.get_fileserver_access_token(repo.id,
|
||||||
|
file_id, 'view', '', use_onetime=False)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
return (False, 500)
|
||||||
|
|
||||||
|
inner_path = gen_inner_file_get_url(token, os.path.basename(path))
|
||||||
|
tmp_path = str(os.path.join(tempfile.gettempdir(), '%s.png' % file_id[:8]))
|
||||||
|
pdf_file = urllib.request.urlopen(inner_path)
|
||||||
|
pdf_stream = BytesIO(pdf_file.read())
|
||||||
|
try:
|
||||||
|
pdf_doc = fitz_open(stream=pdf_stream)
|
||||||
|
pdf_stream.close()
|
||||||
|
page = pdf_doc[0]
|
||||||
|
pix = page.get_pixmap()
|
||||||
|
pix.save(tmp_path)
|
||||||
|
pdf_doc.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return (False, 500)
|
||||||
|
t2 = timeit.default_timer()
|
||||||
|
logger.debug('Create PDF thumbnail of [%s](size: %s) takes: %s' % (path, file_size, (t2 - t1)))
|
||||||
|
|
||||||
|
try:
|
||||||
|
ret = _create_thumbnail_common(tmp_path, thumbnail_file, size)
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
return ret
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
return (False, 500)
|
||||||
|
|
||||||
def create_video_thumbnails(repo, file_id, path, size, thumbnail_file, file_size):
|
def create_video_thumbnails(repo, file_id, path, size, thumbnail_file, file_size):
|
||||||
|
|
||||||
t1 = timeit.default_timer()
|
t1 = timeit.default_timer()
|
||||||
|
@ -1089,6 +1089,7 @@ def react_fake_view(request, **kwargs):
|
|||||||
'ocm_remote_servers': OCM_REMOTE_SERVERS,
|
'ocm_remote_servers': OCM_REMOTE_SERVERS,
|
||||||
'enable_share_to_department': settings.ENABLE_SHARE_TO_DEPARTMENT,
|
'enable_share_to_department': settings.ENABLE_SHARE_TO_DEPARTMENT,
|
||||||
'enable_video_thumbnail': settings.ENABLE_VIDEO_THUMBNAIL,
|
'enable_video_thumbnail': settings.ENABLE_VIDEO_THUMBNAIL,
|
||||||
|
'enable_pdf_thumbnail': settings.ENABLE_PDF_THUMBNAIL,
|
||||||
'group_import_members_extra_msg': GROUP_IMPORT_MEMBERS_EXTRA_MSG,
|
'group_import_members_extra_msg': GROUP_IMPORT_MEMBERS_EXTRA_MSG,
|
||||||
'request_from_onlyoffice_desktop_editor': ONLYOFFICE_DESKTOP_EDITOR_HTTP_USER_AGENT in request.headers.get('user-agent', ''),
|
'request_from_onlyoffice_desktop_editor': ONLYOFFICE_DESKTOP_EDITOR_HTTP_USER_AGENT in request.headers.get('user-agent', ''),
|
||||||
'enable_sso_to_thirdpart_website': settings.ENABLE_SSO_TO_THIRDPART_WEBSITE,
|
'enable_sso_to_thirdpart_website': settings.ENABLE_SSO_TO_THIRDPART_WEBSITE,
|
||||||
|
@ -765,6 +765,8 @@ def view_lib_file(request, repo_id, path):
|
|||||||
send_file_access_msg(request, repo, path, 'web')
|
send_file_access_msg(request, repo, path, 'web')
|
||||||
if filetype == VIDEO:
|
if filetype == VIDEO:
|
||||||
return_dict['enable_video_thumbnail'] = settings.ENABLE_VIDEO_THUMBNAIL
|
return_dict['enable_video_thumbnail'] = settings.ENABLE_VIDEO_THUMBNAIL
|
||||||
|
if filetype == PDF:
|
||||||
|
return_dict['enable_pdf_thumbnail'] = settings.ENABLE_PDF_THUMBNAIL
|
||||||
return render(request, template, return_dict)
|
return render(request, template, return_dict)
|
||||||
|
|
||||||
elif filetype == XMIND:
|
elif filetype == XMIND:
|
||||||
|
@ -31,7 +31,7 @@ from seahub.settings import ENABLE_UPLOAD_FOLDER, \
|
|||||||
THUMBNAIL_ROOT, THUMBNAIL_DEFAULT_SIZE, THUMBNAIL_SIZE_FOR_GRID, \
|
THUMBNAIL_ROOT, THUMBNAIL_DEFAULT_SIZE, THUMBNAIL_SIZE_FOR_GRID, \
|
||||||
MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD, SHARE_LINK_EXPIRE_DAYS_MIN, \
|
MAX_NUMBER_OF_FILES_FOR_FILEUPLOAD, SHARE_LINK_EXPIRE_DAYS_MIN, \
|
||||||
SHARE_LINK_EXPIRE_DAYS_MAX, SEAFILE_COLLAB_SERVER, \
|
SHARE_LINK_EXPIRE_DAYS_MAX, SEAFILE_COLLAB_SERVER, \
|
||||||
ENABLE_SHARE_LINK_REPORT_ABUSE
|
ENABLE_SHARE_LINK_REPORT_ABUSE, ENABLE_PDF_THUMBNAIL
|
||||||
from seahub.utils.file_types import IMAGE, VIDEO, XMIND
|
from seahub.utils.file_types import IMAGE, VIDEO, XMIND
|
||||||
from seahub.thumbnail.utils import get_share_link_thumbnail_src
|
from seahub.thumbnail.utils import get_share_link_thumbnail_src
|
||||||
from seahub.group.utils import is_group_admin
|
from seahub.group.utils import is_group_admin
|
||||||
@ -363,6 +363,7 @@ def view_shared_dir(request, fileshare):
|
|||||||
'desc_for_ogp': desc_for_ogp,
|
'desc_for_ogp': desc_for_ogp,
|
||||||
'enable_share_link_report_abuse': ENABLE_SHARE_LINK_REPORT_ABUSE,
|
'enable_share_link_report_abuse': ENABLE_SHARE_LINK_REPORT_ABUSE,
|
||||||
'enable_video_thumbnail': ENABLE_VIDEO_THUMBNAIL,
|
'enable_video_thumbnail': ENABLE_VIDEO_THUMBNAIL,
|
||||||
|
'enable_pdf_thumbnail': ENABLE_PDF_THUMBNAIL,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,3 +7,4 @@ splinter
|
|||||||
pytest==7.4.4
|
pytest==7.4.4
|
||||||
pytest-django
|
pytest-django
|
||||||
selenium
|
selenium
|
||||||
|
PyMuPDF==1.24.3
|
Loading…
Reference in New Issue
Block a user