mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge 17d99a61da
into 7d84185d0e
This commit is contained in:
commit
aac82d335e
4 changed files with 232 additions and 6 deletions
|
@ -28,6 +28,9 @@ import 'elements/emby-select/emby-select';
|
||||||
|
|
||||||
import 'material-design-icons-iconfont';
|
import 'material-design-icons-iconfont';
|
||||||
import '../formdialog.scss';
|
import '../formdialog.scss';
|
||||||
|
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||||
|
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||||
|
import type { PlaylistUserPermissions } from '@jellyfin/sdk/lib/generated-client/models/playlist-user-permissions';
|
||||||
|
|
||||||
interface DialogElement extends HTMLDivElement {
|
interface DialogElement extends HTMLDivElement {
|
||||||
playlistId?: string
|
playlistId?: string
|
||||||
|
@ -97,6 +100,7 @@ function createPlaylist(dlg: DialogElement) {
|
||||||
createPlaylistDto: {
|
createPlaylistDto: {
|
||||||
Name: name,
|
Name: name,
|
||||||
IsPublic: dlg.querySelector<HTMLInputElement>('#chkPlaylistPublic')?.checked,
|
IsPublic: dlg.querySelector<HTMLInputElement>('#chkPlaylistPublic')?.checked,
|
||||||
|
Users: getUsers(dlg),
|
||||||
Ids: itemIds?.split(','),
|
Ids: itemIds?.split(','),
|
||||||
UserId: apiClient.getCurrentUserId()
|
UserId: apiClient.getCurrentUserId()
|
||||||
}
|
}
|
||||||
|
@ -127,6 +131,7 @@ function updatePlaylist(dlg: DialogElement) {
|
||||||
playlistId: dlg.playlistId,
|
playlistId: dlg.playlistId,
|
||||||
updatePlaylistDto: {
|
updatePlaylistDto: {
|
||||||
Name: name,
|
Name: name,
|
||||||
|
Users: getUsers(dlg),
|
||||||
IsPublic: dlg.querySelector<HTMLInputElement>('#chkPlaylistPublic')?.checked
|
IsPublic: dlg.querySelector<HTMLInputElement>('#chkPlaylistPublic')?.checked
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -279,6 +284,21 @@ function getEditorHtml(items: string[], options: PlaylistEditorOptions) {
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div>
|
||||||
|
<div class="sectionTitleContainer flex align-items-center">
|
||||||
|
<h2 className='sectionTitle'>
|
||||||
|
Users
|
||||||
|
</h2>
|
||||||
|
<button id="btnAddUser" is="emby-button" class="fab submit sectionTitleButton">
|
||||||
|
<span class="material-icons add" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sharesList paperList"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
// newPlaylistInfo
|
// newPlaylistInfo
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
|
@ -295,6 +315,94 @@ function getEditorHtml(items: string[], options: PlaylistEditorOptions) {
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPlaylistPermissionsHtml() {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
html += '<div class="selectContainer-inline">';
|
||||||
|
|
||||||
|
html += '<select is="emby-select">';
|
||||||
|
|
||||||
|
html += '<option value="0">Read</option>';
|
||||||
|
html += '<option value="1">Edit</option>';
|
||||||
|
|
||||||
|
html += '</select>';
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUsers(page: DialogElement): PlaylistUserPermissions[] {
|
||||||
|
return Array.prototype.map.call(page.querySelectorAll('.playlistUser'), function (elem) {
|
||||||
|
return {
|
||||||
|
UserId: elem.getAttribute('data-user-id'),
|
||||||
|
CanEdit: Boolean(parseInt(elem.querySelector('select').value, 10))
|
||||||
|
};
|
||||||
|
}) as PlaylistUserPermissions[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserImage(user: UserDto) {
|
||||||
|
const apiClient = ServerConnections.currentApiClient();
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (apiClient && user.Id) {
|
||||||
|
let imageUrl = 'assets/img/avatar.png';
|
||||||
|
if (user.PrimaryImageTag) {
|
||||||
|
imageUrl = apiClient.getUserImageUrl(user.Id, {
|
||||||
|
width: 35,
|
||||||
|
tag: user.PrimaryImageTag,
|
||||||
|
type: 'Primary'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<img src="${imageUrl}" width="35" height="35" style="border-radius: 100em">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUser(content: DialogElement, user: UserDto, canEdit?: boolean) {
|
||||||
|
const sharesList = content.querySelector('.sharesList');
|
||||||
|
if (sharesList) {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
html += `<div class="listItem playlistUser" data-user-id="${user.Id}">`;
|
||||||
|
|
||||||
|
html += '<div class="listItemBody">';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
${getUserImage(user)}
|
||||||
|
${user.Name}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
${getPlaylistPermissionsHtml()}
|
||||||
|
<button class="btnDelete listItemButton" is="paper-icon-button-light" type="button" title="Delete">
|
||||||
|
<span class="material-icons delete" aria-hidden="true"></span>
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
sharesList.insertAdjacentHTML('beforeend', html);
|
||||||
|
const userElement = sharesList.querySelector(`[data-user-id="${user.Id}"]`);
|
||||||
|
|
||||||
|
userElement?.querySelector('.btnDelete')?.addEventListener('click', () => {
|
||||||
|
userElement.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canEdit) {
|
||||||
|
const selectElement = userElement?.querySelector('select');
|
||||||
|
if (selectElement) {
|
||||||
|
selectElement.value = canEdit ? '1' : '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initEditor(content: DialogElement, options: PlaylistEditorOptions, items: string[]) {
|
function initEditor(content: DialogElement, options: PlaylistEditorOptions, items: string[]) {
|
||||||
content.querySelector('#selectPlaylistToAddTo')?.addEventListener('change', function(this: HTMLSelectElement) {
|
content.querySelector('#selectPlaylistToAddTo')?.addEventListener('change', function(this: HTMLSelectElement) {
|
||||||
if (this.value) {
|
if (this.value) {
|
||||||
|
@ -306,7 +414,35 @@ function initEditor(content: DialogElement, options: PlaylistEditorOptions, item
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||||
|
const api = toApi(apiClient);
|
||||||
|
|
||||||
content.querySelector('form')?.addEventListener('submit', onSubmit);
|
content.querySelector('form')?.addEventListener('submit', onSubmit);
|
||||||
|
content.querySelector('#btnAddUser')?.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const shareUsers = getUsers(content).map(user => user.UserId);
|
||||||
|
|
||||||
|
const users = getUserApi(api).getUsers().then(req => {
|
||||||
|
return req.data.filter(user => user.Id != apiClient.getCurrentUserId() && !shareUsers.includes(user.Id));
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[PlaylistEditor] failed to fetch users', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
import('../userpicker/userpicker').then(({ default: UserPicker }) => {
|
||||||
|
const picker = new UserPicker();
|
||||||
|
|
||||||
|
picker.show({
|
||||||
|
users: users,
|
||||||
|
callback: function (selectedUser: UserDto) {
|
||||||
|
addUser(content, selectedUser);
|
||||||
|
picker.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
console.error('[PlaylistEditor] failed to show user picker');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const selectedItemsInput = content.querySelector<HTMLInputElement>('.fldSelectedItemIds');
|
const selectedItemsInput = content.querySelector<HTMLInputElement>('.fldSelectedItemIds');
|
||||||
if (selectedItemsInput) {
|
if (selectedItemsInput) {
|
||||||
|
@ -327,16 +463,15 @@ function initEditor(content: DialogElement, options: PlaylistEditorOptions, item
|
||||||
console.error('[PlaylistEditor] could not find dialog element');
|
console.error('[PlaylistEditor] could not find dialog element');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
|
||||||
const api = toApi(apiClient);
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
getUserLibraryApi(api)
|
getUserLibraryApi(api)
|
||||||
.getItem({ itemId: options.id }),
|
.getItem({ itemId: options.id }),
|
||||||
getPlaylistsApi(api)
|
getPlaylistsApi(api)
|
||||||
.getPlaylist({ playlistId: options.id })
|
.getPlaylist({ playlistId: options.id }),
|
||||||
|
getUserApi(api)
|
||||||
|
.getUsers()
|
||||||
])
|
])
|
||||||
.then(([ { data: playlistItem }, { data: playlist } ]) => {
|
.then(([ { data: playlistItem }, { data: playlist }, { data: users } ]) => {
|
||||||
panel.playlistId = options.id;
|
panel.playlistId = options.id;
|
||||||
|
|
||||||
const nameField = panel.querySelector<HTMLInputElement>('#txtNewPlaylistName');
|
const nameField = panel.querySelector<HTMLInputElement>('#txtNewPlaylistName');
|
||||||
|
@ -344,6 +479,14 @@ function initEditor(content: DialogElement, options: PlaylistEditorOptions, item
|
||||||
|
|
||||||
const publicField = panel.querySelector<HTMLInputElement>('#chkPlaylistPublic');
|
const publicField = panel.querySelector<HTMLInputElement>('#chkPlaylistPublic');
|
||||||
if (publicField) publicField.checked = !!playlist.OpenAccess;
|
if (publicField) publicField.checked = !!playlist.OpenAccess;
|
||||||
|
|
||||||
|
playlist.Shares?.forEach(shareUser => {
|
||||||
|
const user = users.find(u => u.Id == shareUser.UserId);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
addUser(panel, user, shareUser.CanEdit);
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('[playlistEditor] failed to get playlist details', err);
|
console.error('[playlistEditor] failed to get playlist details', err);
|
||||||
|
|
60
src/components/userpicker/userpicker.js
Normal file
60
src/components/userpicker/userpicker.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import dialogHelper from 'components/dialogHelper/dialogHelper';
|
||||||
|
import globalize from 'lib/globalize';
|
||||||
|
import template from './userpicker.template.html';
|
||||||
|
|
||||||
|
function initUsers(page, users) {
|
||||||
|
const userSelect = page.querySelector('#selectUser');
|
||||||
|
|
||||||
|
const userOptionsHtml = users.map(function (user) {
|
||||||
|
return '<option value="' + user.Id + '">' + user.Name + '</option>';
|
||||||
|
});
|
||||||
|
|
||||||
|
userSelect.innerHTML += userOptionsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserPicker {
|
||||||
|
show = (options) => {
|
||||||
|
if (options.users != null) {
|
||||||
|
options.users.then(users => {
|
||||||
|
const dlg = dialogHelper.createDialog({
|
||||||
|
size: 'small',
|
||||||
|
removeOnClose: true,
|
||||||
|
scrollY: true
|
||||||
|
});
|
||||||
|
dlg.classList.add('ui-body-a');
|
||||||
|
dlg.classList.add('background-theme-a');
|
||||||
|
dlg.classList.add('formDialog');
|
||||||
|
dlg.innerHTML = globalize.translateHtml(template);
|
||||||
|
this.currentDialog = dlg;
|
||||||
|
initUsers(dlg, users);
|
||||||
|
dialogHelper.open(dlg)
|
||||||
|
.catch(err => {
|
||||||
|
console.log('[userpicker] failed to open dialog', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
dlg.querySelector('.btnCancel')?.addEventListener('click', () => {
|
||||||
|
dialogHelper.close(dlg);
|
||||||
|
});
|
||||||
|
dlg.querySelector('.btnCloseDialog')?.addEventListener('click', () => {
|
||||||
|
dialogHelper.close(dlg);
|
||||||
|
});
|
||||||
|
dlg.querySelector('form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const selectedUserId = this.querySelector('#selectUser')?.value;
|
||||||
|
if (selectedUserId && options.callback) {
|
||||||
|
const selectedUser = users.find(user => user.Id == selectedUserId);
|
||||||
|
options.callback(selectedUser);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
if (this.currentDialog) {
|
||||||
|
dialogHelper.close(this.currentDialog);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserPicker;
|
22
src/components/userpicker/userpicker.template.html
Normal file
22
src/components/userpicker/userpicker.template.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<div class="formDialogHeader">
|
||||||
|
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}">
|
||||||
|
<span class="material-icons arrow_back" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<h3 class="formDialogHeaderTitle">Select User</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formDialogContent scrollY" style="padding-top:2em;">
|
||||||
|
<div class="dialogContentInner dialog-content-centered">
|
||||||
|
<form>
|
||||||
|
<div class="selectContainer">
|
||||||
|
<select is="emby-select" id="selectUser" label="User"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formDialogFooter">
|
||||||
|
<button is="emby-button" type="submit" class="raised button-submit block formDialogFooterItem">
|
||||||
|
<span>${Add}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -75,6 +75,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectContainer-inline {
|
.selectContainer-inline {
|
||||||
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -117,7 +118,7 @@
|
||||||
|
|
||||||
.selectContainer-inline > .selectArrowContainer {
|
.selectContainer-inline > .selectArrowContainer {
|
||||||
top: initial;
|
top: initial;
|
||||||
bottom: 0.24em;
|
bottom: 0.03em;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue