This commit is contained in:
Markos Gogoulos
2026-03-15 17:09:51 +02:00
parent c1ce525f24
commit a79acb5ab0
8 changed files with 319 additions and 2 deletions

View File

@@ -576,6 +576,7 @@
// Ensure icon buttons are visible on mobile
&.media-search,
&.media-filters-toggle,
&.media-sharing-toggle,
&.media-tags-toggle,
&.media-sorting-toggle {
@media screen and (max-width: 768px) {
@@ -901,6 +902,13 @@ $-max-width: $-hor-spaces + ( 2 * $item-width ) - 1;
}
}
.mi-sharing-filter-options {
> .active button,
> * button:hover {
background-color: #3b82f6 !important;
}
}
$-hor-spaces: 2 * $side-empty-space;
$-max-width: $-hor-spaces + ( 2 * $item-width ) - 1;

View File

@@ -491,6 +491,39 @@ class NavMenuInlineTabs extends React.PureComponent {
</span>
</li>
) : null}
{this.props.onToggleSharingClick &&
['media', 'shared_by_me'].includes(this.props.type) ? (
<li className="media-sharing-toggle">
<span
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
position: 'relative',
}}
onClick={this.props.onToggleSharingClick}
title={translateString('Shared with')}
>
<CircleIconButton buttonShadow={false}>
<i className="material-icons">people</i>
</CircleIconButton>
{this.props.hasActiveSharing ? (
<span
style={{
position: 'absolute',
top: '8px',
right: '8px',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)',
border: '2px solid white',
}}
></span>
) : null}
</span>
</li>
) : null}
{this.props.onToggleTagsClick &&
['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-tags-toggle">
@@ -570,9 +603,11 @@ NavMenuInlineTabs.propTypes = {
type: PropTypes.string.isRequired,
onQueryChange: PropTypes.func,
onToggleFiltersClick: PropTypes.func,
onToggleSharingClick: PropTypes.func,
onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveSharing: PropTypes.bool,
hasActiveTags: PropTypes.bool,
hasActiveSort: PropTypes.bool,
};
@@ -776,9 +811,11 @@ export default function ProfilePagesHeader(props) {
type={props.type}
onQueryChange={props.onQueryChange}
onToggleFiltersClick={props.onToggleFiltersClick}
onToggleSharingClick={userIsAuthor ? props.onToggleSharingClick : undefined}
onToggleTagsClick={props.onToggleTagsClick}
onToggleSortingClick={props.onToggleSortingClick}
hasActiveFilters={props.hasActiveFilters}
hasActiveSharing={props.hasActiveSharing}
hasActiveTags={props.hasActiveTags}
hasActiveSort={props.hasActiveSort}
/>
@@ -792,9 +829,11 @@ ProfilePagesHeader.propTypes = {
type: PropTypes.string.isRequired,
onQueryChange: PropTypes.func,
onToggleFiltersClick: PropTypes.func,
onToggleSharingClick: PropTypes.func,
onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveSharing: PropTypes.bool,
hasActiveTags: PropTypes.bool,
hasActiveSort: PropTypes.bool,
};

View File

@@ -0,0 +1,101 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { PageStore } from '../../utils/stores/';
import { FilterOptions } from '../_shared';
import { translateString } from '../../utils/helpers/';
import '../management-table/ManageItemList-filters.scss';
export function ProfileMediaSharing(props) {
const [isHidden, setIsHidden] = useState(props.hidden);
const containerRef = useRef(null);
const innerContainerRef = useRef(null);
function onWindowResize() {
if (!isHidden && containerRef.current && innerContainerRef.current) {
containerRef.current.style.height = 24 + innerContainerRef.current.offsetHeight + 'px';
}
}
useEffect(() => {
setIsHidden(props.hidden);
onWindowResize();
}, [props.hidden, props.sharedUsers, props.sharedGroups]);
useEffect(() => {
PageStore.on('window_resize', onWindowResize);
return () => PageStore.removeListener('window_resize', onWindowResize);
}, []);
function onUserSelect(ev) {
const username = ev.currentTarget.getAttribute('value');
const newValue = (username === 'all' || username === props.selectedSharingValue) ? null : username;
props.onSharingSelect(newValue ? 'user' : null, newValue);
}
function onGroupSelect(ev) {
const name = ev.currentTarget.getAttribute('value');
const newValue = (name === 'all' || name === props.selectedSharingValue) ? null : name;
props.onSharingSelect(newValue ? 'group' : null, newValue);
}
const hasUsers = props.sharedUsers && props.sharedUsers.length > 0;
const hasGroups = props.sharedGroups && props.sharedGroups.length > 0;
const usersOptions = [
{ id: 'all', title: translateString('All') },
...(props.sharedUsers || []).map((u) => ({ id: u.username, title: u.name })),
];
const groupsOptions = [
{ id: 'all', title: translateString('All') },
...(props.sharedGroups || []).map((g) => ({ id: g.name, title: g.name })),
];
const selectedUser = props.selectedSharingType === 'user' ? props.selectedSharingValue : 'all';
const selectedGroup = props.selectedSharingType === 'group' ? props.selectedSharingValue : 'all';
return (
<div ref={containerRef} className={'mi-filters-row' + (isHidden ? ' hidden' : '')}>
<div ref={innerContainerRef} className="mi-filters-row-inner">
{hasUsers ? (
<div className="mi-filter mi-filter-full-width">
<div className="mi-filter-title">{translateString('SHARED WITH USERS')}</div>
<div className="mi-filter-options mi-filter-options-horizontal mi-sharing-filter-options">
<FilterOptions id="shared_user" options={usersOptions} selected={selectedUser} onSelect={onUserSelect} />
</div>
</div>
) : null}
{hasGroups ? (
<div className="mi-filter mi-filter-full-width">
<div className="mi-filter-title">{translateString('SHARED WITH GROUPS')}</div>
<div className="mi-filter-options mi-filter-options-horizontal mi-sharing-filter-options">
<FilterOptions id="shared_group" options={groupsOptions} selected={selectedGroup} onSelect={onGroupSelect} />
</div>
</div>
) : null}
{!hasUsers && !hasGroups ? (
<div className="mi-filter mi-filter-full-width">
<div className="mi-filter-title">{translateString('NOT SHARED WITH ANYONE')}</div>
</div>
) : null}
</div>
</div>
);
}
ProfileMediaSharing.propTypes = {
hidden: PropTypes.bool,
sharedUsers: PropTypes.array,
sharedGroups: PropTypes.array,
onSharingSelect: PropTypes.func,
selectedSharingType: PropTypes.string,
selectedSharingValue: PropTypes.string,
};
ProfileMediaSharing.defaultProps = {
hidden: false,
sharedUsers: [],
sharedGroups: [],
selectedSharingType: null,
selectedSharingValue: null,
};

View File

@@ -11,6 +11,7 @@ import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListA
import { BulkActionsModals } from '../components/BulkActionsModals';
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
import { ProfileMediaSharing } from '../components/search-filters/ProfileMediaSharing';
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
import { withBulkActions } from '../utils/hoc/withBulkActions';
@@ -35,10 +36,15 @@ class ProfileMediaPage extends Page {
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: true,
hiddenSharing: true,
filterArgs: '',
availableTags: [],
selectedTag: 'all',
selectedSort: 'date_added_desc',
sharedUsers: [],
sharedGroups: [],
selectedSharingType: null,
selectedSharingValue: null,
};
this.authorDataLoad = this.authorDataLoad.bind(this);
@@ -49,9 +55,11 @@ class ProfileMediaPage extends Page {
this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this);
this.onToggleTagsClick = this.onToggleTagsClick.bind(this);
this.onToggleSortingClick = this.onToggleSortingClick.bind(this);
this.onToggleSharingClick = this.onToggleSharingClick.bind(this);
this.onFiltersUpdate = this.onFiltersUpdate.bind(this);
this.onTagSelect = this.onTagSelect.bind(this);
this.onSortSelect = this.onSortSelect.bind(this);
this.onSharingSelect = this.onSharingSelect.bind(this);
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
ProfilePageStore.on('load-author-data', this.authorDataLoad);
@@ -178,6 +186,7 @@ class ProfileMediaPage extends Page {
hiddenFilters: !this.state.hiddenFilters,
hiddenTags: true,
hiddenSorting: true,
hiddenSharing: true,
});
}
@@ -186,6 +195,7 @@ class ProfileMediaPage extends Page {
hiddenFilters: true,
hiddenTags: !this.state.hiddenTags,
hiddenSorting: true,
hiddenSharing: true,
});
}
@@ -194,6 +204,16 @@ class ProfileMediaPage extends Page {
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: !this.state.hiddenSorting,
hiddenSharing: true,
});
}
onToggleSharingClick() {
this.setState({
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: true,
hiddenSharing: !this.state.hiddenSharing,
});
}
@@ -214,6 +234,8 @@ class ProfileMediaPage extends Page {
: null,
sort_by: this.state.selectedSort,
tag: tag,
sharing_type: this.state.selectedSharingType,
sharing_value: this.state.selectedSharingValue,
});
});
}
@@ -235,6 +257,31 @@ class ProfileMediaPage extends Page {
: null,
sort_by: sortOption,
tag: this.state.selectedTag,
sharing_type: this.state.selectedSharingType,
sharing_value: this.state.selectedSharingValue,
});
});
}
onSharingSelect(type, value) {
this.setState({ selectedSharingType: type, selectedSharingValue: value }, () => {
this.onFiltersUpdate({
media_type: this.state.filterArgs.includes('media_type')
? this.state.filterArgs.match(/media_type=([^&]*)/)?.[1]
: null,
upload_date: this.state.filterArgs.includes('upload_date')
? this.state.filterArgs.match(/upload_date=([^&]*)/)?.[1]
: null,
duration: this.state.filterArgs.includes('duration')
? this.state.filterArgs.match(/duration=([^&]*)/)?.[1]
: null,
publish_state: this.state.filterArgs.includes('publish_state')
? this.state.filterArgs.match(/publish_state=([^&]*)/)?.[1]
: null,
sort_by: this.state.selectedSort,
tag: this.state.selectedTag,
sharing_type: type,
sharing_value: value,
});
});
}
@@ -248,6 +295,8 @@ class ProfileMediaPage extends Page {
sort_by: null,
ordering: null,
t: null,
shared_user: null,
shared_group: null,
};
switch (updatedArgs.media_type) {
@@ -306,6 +355,12 @@ class ProfileMediaPage extends Page {
args.t = updatedArgs.tag;
}
if (updatedArgs.sharing_type === 'user' && updatedArgs.sharing_value) {
args.shared_user = updatedArgs.sharing_value;
} else if (updatedArgs.sharing_type === 'group' && updatedArgs.sharing_value) {
args.shared_group = updatedArgs.sharing_value;
}
const newArgs = [];
for (let arg in args) {
@@ -353,6 +408,12 @@ class ProfileMediaPage extends Page {
.filter((tag) => tag);
this.setState({ availableTags: tags });
}
if (responseData && responseData.shared_users !== undefined) {
this.setState({
sharedUsers: responseData.shared_users || [],
sharedGroups: responseData.shared_groups || [],
});
}
}
pageContent() {
@@ -381,9 +442,11 @@ class ProfileMediaPage extends Page {
onToggleFiltersClick={this.onToggleFiltersClick}
onToggleTagsClick={this.onToggleTagsClick}
onToggleSortingClick={this.onToggleSortingClick}
onToggleSharingClick={this.onToggleSharingClick}
hasActiveFilters={hasActiveFilters}
hasActiveTags={hasActiveTags}
hasActiveSort={hasActiveSort}
hasActiveSharing={!!this.state.selectedSharingValue}
hideChannelBanner={inEmbeddedApp()}
/>
) : null,
@@ -414,6 +477,14 @@ class ProfileMediaPage extends Page {
onTagSelect={this.onTagSelect}
/>
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
<ProfileMediaSharing
hidden={this.state.hiddenSharing}
sharedUsers={this.state.sharedUsers}
sharedGroups={this.state.sharedGroups}
onSharingSelect={this.onSharingSelect}
selectedSharingType={this.state.selectedSharingType}
selectedSharingValue={this.state.selectedSharingValue}
/>
<LazyLoadItemListAsync
key={`${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
requestUrl={this.state.requestUrl}

View File

@@ -9,6 +9,7 @@ import ProfilePagesContent from '../components/profile-page/ProfilePagesContent'
import { LazyLoadItemListAsync } from '../components/item-list/LazyLoadItemListAsync';
import { ProfileMediaFilters } from '../components/search-filters/ProfileMediaFilters';
import { ProfileMediaTags } from '../components/search-filters/ProfileMediaTags';
import { ProfileMediaSharing } from '../components/search-filters/ProfileMediaSharing';
import { ProfileMediaSorting } from '../components/search-filters/ProfileMediaSorting';
import { BulkActionsModals } from '../components/BulkActionsModals';
import { inEmbeddedApp, inSelectMediaEmbedMode } from '../utils/helpers';
@@ -47,11 +48,16 @@ class ProfileSharedByMePage extends Page {
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: true,
hiddenSharing: true,
filterArgs: '',
availableTags: [],
selectedTag: 'all',
selectedSort: 'date_added_desc',
selectedMedia: new Set(), // For select media mode
sharedUsers: [],
sharedGroups: [],
selectedSharingType: null,
selectedSharingValue: null,
};
this.authorDataLoad = this.authorDataLoad.bind(this);
@@ -61,9 +67,11 @@ class ProfileSharedByMePage extends Page {
this.onToggleFiltersClick = this.onToggleFiltersClick.bind(this);
this.onToggleTagsClick = this.onToggleTagsClick.bind(this);
this.onToggleSortingClick = this.onToggleSortingClick.bind(this);
this.onToggleSharingClick = this.onToggleSharingClick.bind(this);
this.onFiltersUpdate = this.onFiltersUpdate.bind(this);
this.onTagSelect = this.onTagSelect.bind(this);
this.onSortSelect = this.onSortSelect.bind(this);
this.onSharingSelect = this.onSharingSelect.bind(this);
this.onResponseDataLoaded = this.onResponseDataLoaded.bind(this);
this.handleMediaSelection = this.handleMediaSelection.bind(this);
@@ -177,6 +185,7 @@ class ProfileSharedByMePage extends Page {
hiddenFilters: !this.state.hiddenFilters,
hiddenTags: true,
hiddenSorting: true,
hiddenSharing: true,
});
}
@@ -185,6 +194,7 @@ class ProfileSharedByMePage extends Page {
hiddenFilters: true,
hiddenTags: !this.state.hiddenTags,
hiddenSorting: true,
hiddenSharing: true,
});
}
@@ -193,6 +203,16 @@ class ProfileSharedByMePage extends Page {
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: !this.state.hiddenSorting,
hiddenSharing: true,
});
}
onToggleSharingClick() {
this.setState({
hiddenFilters: true,
hiddenTags: true,
hiddenSorting: true,
hiddenSharing: !this.state.hiddenSharing,
});
}
@@ -205,6 +225,8 @@ class ProfileSharedByMePage extends Page {
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: this.state.selectedSort,
tag: tag,
sharing_type: this.state.selectedSharingType,
sharing_value: this.state.selectedSharingValue,
});
});
}
@@ -218,6 +240,23 @@ class ProfileSharedByMePage extends Page {
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: sortBy,
tag: this.state.selectedTag,
sharing_type: this.state.selectedSharingType,
sharing_value: this.state.selectedSharingValue,
});
});
}
onSharingSelect(type, value) {
this.setState({ selectedSharingType: type, selectedSharingValue: value }, () => {
this.onFiltersUpdate({
media_type: this.state.filterArgs.match(/media_type=([^&]+)/)?.[1],
upload_date: this.state.filterArgs.match(/upload_date=([^&]+)/)?.[1],
duration: this.state.filterArgs.match(/duration=([^&]+)/)?.[1],
publish_state: this.state.filterArgs.match(/publish_state=([^&]+)/)?.[1],
sort_by: this.state.selectedSort,
tag: this.state.selectedTag,
sharing_type: type,
sharing_value: value,
});
});
}
@@ -231,6 +270,8 @@ class ProfileSharedByMePage extends Page {
sort_by: null,
ordering: null,
t: null,
shared_user: null,
shared_group: null,
};
switch (updatedArgs.media_type) {
@@ -292,6 +333,12 @@ class ProfileSharedByMePage extends Page {
args.t = updatedArgs.tag;
}
if (updatedArgs.sharing_type === 'user' && updatedArgs.sharing_value) {
args.shared_user = updatedArgs.sharing_value;
} else if (updatedArgs.sharing_type === 'group' && updatedArgs.sharing_value) {
args.shared_group = updatedArgs.sharing_value;
}
const newArgs = [];
for (let arg in args) {
@@ -343,6 +390,12 @@ class ProfileSharedByMePage extends Page {
.filter((tag) => tag);
this.setState({ availableTags: tags });
}
if (responseData && responseData.shared_users !== undefined) {
this.setState({
sharedUsers: responseData.shared_users || [],
sharedGroups: responseData.shared_groups || [],
});
}
}
handleMediaSelection(mediaId, isSelected) {
@@ -413,9 +466,11 @@ class ProfileSharedByMePage extends Page {
onToggleFiltersClick={this.onToggleFiltersClick}
onToggleTagsClick={this.onToggleTagsClick}
onToggleSortingClick={this.onToggleSortingClick}
onToggleSharingClick={this.onToggleSharingClick}
hasActiveFilters={hasActiveFilters}
hasActiveTags={this.state.selectedTag !== 'all'}
hasActiveSort={this.state.selectedSort !== 'date_added_desc'}
hasActiveSharing={!!this.state.selectedSharingValue}
hideChannelBanner={inEmbeddedApp()}
/>
) : null,
@@ -443,6 +498,14 @@ class ProfileSharedByMePage extends Page {
onTagSelect={this.onTagSelect}
/>
<ProfileMediaSorting hidden={this.state.hiddenSorting} onSortSelect={this.onSortSelect} />
<ProfileMediaSharing
hidden={this.state.hiddenSharing}
sharedUsers={this.state.sharedUsers}
sharedGroups={this.state.sharedGroups}
onSharingSelect={this.onSharingSelect}
selectedSharingType={this.state.selectedSharingType}
selectedSharingValue={this.state.selectedSharingValue}
/>
<LazyLoadItemListAsync
key={isSelectMediaMode ? this.state.requestUrl : `${this.state.requestUrl}-${this.props.bulkActions.listKey}`}
requestUrl={this.state.requestUrl}