mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 00:47:09 +00:00
improve folder view - test with display-api
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
title = '',
|
||||
click_function = (e: MouseEvent) => {},
|
||||
menu_options = null,
|
||||
menu_class = 'right-0',
|
||||
div_class = '',
|
||||
children
|
||||
} = $props<{
|
||||
className?: string;
|
||||
@@ -23,6 +25,8 @@
|
||||
title?: string;
|
||||
click_function?: (e: MouseEvent) => void;
|
||||
menu_options?: MenuOption[] | null;
|
||||
menu_class?: string;
|
||||
div_class?: string,
|
||||
children?: any;
|
||||
}>();
|
||||
|
||||
@@ -126,7 +130,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative min-w-0 flex" {title}>
|
||||
<div class="relative min-w-0 flex {div_class}" {title}>
|
||||
<button
|
||||
bind:this={button_element}
|
||||
class="{className} {menu_shown ? hover_bg : bg} {disabled
|
||||
@@ -150,7 +154,7 @@
|
||||
transition:fade={{ duration: 50 }}
|
||||
class="absolute {position_bottom
|
||||
? 'top-full'
|
||||
: 'bottom-full'} right-0 z-100 my-1.5 min-w-64 rounded-xl backdrop-blur bg-stone-600/30 border border-stone-400/10 shadow-xl/20 p-2 flex flex-col gap-2 text-stone-200 cursor-auto"
|
||||
: 'bottom-full'} {menu_class} z-100 my-1.5 min-w-64 rounded-xl backdrop-blur bg-stone-600/30 border border-stone-400/10 shadow-xl/20 p-2 flex flex-col gap-2 text-stone-200 cursor-auto"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
@@ -175,7 +179,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="pr-2">
|
||||
<div class="truncate min-w-0" title={option.name}>
|
||||
{option.name}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -7,16 +7,14 @@
|
||||
import {
|
||||
add_empty_display_group,
|
||||
all_displays_of_group_selected,
|
||||
is_selected,
|
||||
remove_empty_display_groups,
|
||||
select,
|
||||
select_all_of_group,
|
||||
selected_display_ids,
|
||||
set_new_display_group_data
|
||||
} from '../ts/stores/displays';
|
||||
import DNDGrip from './DNDGrip.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { DisplayGroup } from '../ts/types';
|
||||
import { selected_display_ids } from '../ts/stores/select';
|
||||
|
||||
let { display_group } = $props<{
|
||||
display_group: DisplayGroup;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<script lang="ts">
|
||||
import Button from './Button.svelte';
|
||||
import { is_selected, select, selected_display_ids } from '../ts/stores/displays';
|
||||
import {
|
||||
display_screen_height,
|
||||
current_height,
|
||||
get_selectable_color_classes,
|
||||
pinned_display_id
|
||||
} from '../ts/stores/ui_behavior';
|
||||
import DNDGrip from './DNDGrip.svelte';
|
||||
import { Menu, Pencil, Pin, PinOff, Trash2, VideoOff, X } from 'lucide-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import OnlineState from './OnlineState.svelte';
|
||||
import type { Display } from '../ts/types';
|
||||
import { is_selected, select, selected_display_ids } from '../ts/stores/select';
|
||||
|
||||
let { display } = $props<{
|
||||
display: Display;
|
||||
@@ -19,7 +18,7 @@
|
||||
let hovering_unselectable = $state(false);
|
||||
|
||||
function onclick(e: Event) {
|
||||
select(display.id);
|
||||
select(selected_display_ids, display.id);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
@@ -45,7 +44,7 @@
|
||||
hover: true,
|
||||
active: !hovering_unselectable,
|
||||
text: true
|
||||
})} rounded-xl flex flex-row justify-between h-{$display_screen_height} transition-colors duration-100 gap-2 cursor-pointer w-full text-stone-200"
|
||||
})} rounded-xl flex flex-row justify-between h-{$current_height.display} transition-colors duration-100 gap-2 cursor-pointer w-full text-stone-200"
|
||||
>
|
||||
<div class="flex flex-row gap-4 min-w-0 flex-1">
|
||||
<!-- Left Preview Screen -->
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { CloudAlert, Folder, RefreshCcwDot, Shield } from 'lucide-svelte';
|
||||
import { get_selectable_color_classes } from '../ts/stores/ui_behavior';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
let {} = $props<{}>();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="p-1 {get_selectable_color_classes(false, {
|
||||
bg: true,
|
||||
hover: true,
|
||||
active: true,
|
||||
text: true
|
||||
})} h-10 rounded-lg transition-colors duration-200 gap-4 flex flex-row justify-between cursor-pointer group"
|
||||
>
|
||||
<div class="flex flex-row gap-2 min-w-0">
|
||||
<div class="aspect-square rounded-md flex justify-center items-center">
|
||||
<Folder />
|
||||
</div>
|
||||
<div class="content-center truncate select-none">
|
||||
Tolle
|
||||
Dateifjisodfhsuidfhdufighdfiughdfuighdfuihguifdghiudfhguidfgdfijghdfjkghdfkjghdfkjghdfkughdfjghdfkjghdfkjghfdkghdfkjghdfkghdfkghdfkjghdfkghdfkjghdfgkhdfkghfdkjghdfkghfdkghdfkjghdfjkghdk.zip
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-1 pr-1 text-stone-400 group-hover:text-stone-300 group-active:text-stone-250 duration-200 transition-colors">
|
||||
<Button
|
||||
className="h-8 aspect-square transition-colors duration-200 !p-1.5"
|
||||
bg="bg-transparent"
|
||||
hover_bg={get_selectable_color_classes(false, {
|
||||
bg: true
|
||||
})}
|
||||
active_bg={get_selectable_color_classes(false, {
|
||||
bg: true
|
||||
})}
|
||||
click_function={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<RefreshCcwDot class="size-full" />
|
||||
</Button>
|
||||
<div class="content-center select-none text-xs whitespace-nowrap">25.09.25</div>
|
||||
<div class="border border-stone-550 group-hover:border-stone-450 group-active:border-stone-400 duration-200 transition-colors my-1"></div>
|
||||
<div class="content-center select-none text-xs whitespace-nowrap">MPEG</div>
|
||||
<div class="border border-stone-550 group-hover:border-stone-450 group-active:border-stone-400 duration-200 transition-colors my-1"></div>
|
||||
<div class="content-center select-none text-xs whitespace-nowrap">523 MB</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,223 @@
|
||||
<script lang="ts">
|
||||
import { ArrowRight, Folder, Play, RefreshCcwDot, TriangleAlert } from 'lucide-svelte';
|
||||
import {
|
||||
current_height,
|
||||
get_selectable_color_classes,
|
||||
get_shifted_color
|
||||
} from '../ts/stores/ui_behavior';
|
||||
import Button from './Button.svelte';
|
||||
import { supported_file_types, type FolderElement, type SupportedFileType } from '../ts/types';
|
||||
import {
|
||||
is_selected,
|
||||
select,
|
||||
selected_display_ids,
|
||||
selected_file_ids
|
||||
} from '../ts/stores/select';
|
||||
import {
|
||||
all_files,
|
||||
change_file_path,
|
||||
current_file_path,
|
||||
get_display_ids_where_file_is_missing
|
||||
} from '../ts/stores/files';
|
||||
import RefreshPlay from './RefreshPlay.svelte';
|
||||
|
||||
let { file } = $props<{ file: FolderElement }>();
|
||||
|
||||
const is_folder = file.type === 'inode/directory';
|
||||
|
||||
function get_file_type(file: FolderElement): SupportedFileType | null {
|
||||
for (const key of Object.keys(supported_file_types)) {
|
||||
if (file.type === supported_file_types[key].mime_type) {
|
||||
return supported_file_types[key];
|
||||
}
|
||||
}
|
||||
// Fallback:
|
||||
const extension = file.name.split('.').pop();
|
||||
if (extension) {
|
||||
if (Object.keys(supported_file_types).includes('.' + extension)) {
|
||||
return supported_file_types['.' + extension];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function get_created_string(date_object: Date, full_string = false) {
|
||||
if (full_string) {
|
||||
return (
|
||||
get_formated_date_string(date_object, true) + ' ' + get_formated_time_string(date_object)
|
||||
);
|
||||
} else if (date_object.toDateString() === new Date().toDateString()) {
|
||||
return get_formated_time_string(date_object);
|
||||
} else {
|
||||
return get_formated_date_string(date_object);
|
||||
}
|
||||
}
|
||||
|
||||
function get_formated_time_string(date_object: Date) {
|
||||
return `${date_object.getHours().toString().padStart(2, '0')}:${date_object.getMinutes().toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function get_formated_date_string(date_object: Date, full_year = false) {
|
||||
return `${date_object.getDate().toString().padStart(2, '0')}.${(date_object.getMonth() + 1).toString().padStart(2, '0')}.${date_object
|
||||
.getFullYear()
|
||||
.toString()
|
||||
.slice(full_year ? 0 : 2)}`;
|
||||
}
|
||||
|
||||
function get_grayed_out_text_color_strings(is_selected: boolean): string {
|
||||
const color = is_selected ? 'text-stone-600' : 'text-stone-400';
|
||||
const factor = is_selected ? -1 : 1;
|
||||
return `${color} group-hover:${get_shifted_color(color, factor * 100)} group-active:${get_shifted_color(color, factor * 150)}`;
|
||||
}
|
||||
|
||||
function get_grayed_out_border_color_strings(is_selected: boolean): string {
|
||||
const color = is_selected ? 'border-stone-450' : 'border-stone-550';
|
||||
const factor = is_selected ? 1 : 1;
|
||||
return `${color} group-hover:${get_shifted_color(color, factor * 100)} group-active:${get_shifted_color(color, factor * 150)}`;
|
||||
}
|
||||
|
||||
function onclick(e: Event) {
|
||||
select(selected_file_ids, file.id);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (is_folder) {
|
||||
change_file_path($current_file_path + file.name + '/');
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row h-{$current_height.file} w-full">
|
||||
<div class="h-{$current_height.file} aspect-square max-w-15 flex">
|
||||
<Button
|
||||
className="flex rounded-l-lg rounded-r-none {is_folder
|
||||
? 'text-stone-450'
|
||||
: 'text-stone-800'} w-full"
|
||||
div_class="w-full"
|
||||
bg={get_selectable_color_classes(
|
||||
!is_folder,
|
||||
{
|
||||
bg: true
|
||||
},
|
||||
-50
|
||||
)}
|
||||
hover_bg={get_selectable_color_classes(
|
||||
!is_folder,
|
||||
{
|
||||
bg: true
|
||||
},
|
||||
50
|
||||
)}
|
||||
active_bg={get_selectable_color_classes(
|
||||
!is_folder,
|
||||
{
|
||||
bg: true
|
||||
},
|
||||
100
|
||||
)}
|
||||
click_function={(e) => {
|
||||
open();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{#if is_folder}
|
||||
<ArrowRight class="size-full" strokeWidth="3" />
|
||||
{:else if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[0].length !== 0}
|
||||
<RefreshPlay className="size-full" />
|
||||
{:else}
|
||||
<Play class="size-full" strokeWidth="3" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') onclick(e);
|
||||
}}
|
||||
{onclick}
|
||||
class="{get_selectable_color_classes(is_selected(file.id, $selected_file_ids), {
|
||||
bg: true,
|
||||
hover: true,
|
||||
active: true,
|
||||
text: true
|
||||
})} rounded-r-lg transition-colors duration-200 gap-4 flex flex-row justify-between cursor-pointer group w-full h-full min-w-0"
|
||||
>
|
||||
<div class="flex flex-row gap-2 min-w-0 w-full">
|
||||
<div class="aspect-square rounded-md flex justify-center items-center p-2">
|
||||
{#if is_folder}
|
||||
<Folder class="size-full" />
|
||||
{:else if file.thumbnail}
|
||||
<div></div>
|
||||
{:else if get_file_type(file)?.icon}
|
||||
{@const Icon = get_file_type(file)?.icon}
|
||||
<Icon class="size-full" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="content-center truncate select-none w-full" title={file.name}>
|
||||
{file.name.includes('.') ? file.name.slice(0, file.name.lastIndexOf('.')) : file.name}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class=" p-1 flex flex-row items-center gap-1 pr-1 {get_grayed_out_text_color_strings(
|
||||
is_selected(file.id, $selected_file_ids)
|
||||
)} duration-200 transition-colors"
|
||||
>
|
||||
{#if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[1].length !== 0}
|
||||
<Button
|
||||
className="h-8 aspect-square transition-colors duration-200 !p-1.5 text-stone-100"
|
||||
bg="bg-red-500"
|
||||
click_function={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<TriangleAlert class="size-full" />
|
||||
</Button>
|
||||
{:else if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[0].length !== 0}
|
||||
<Button
|
||||
className="h-8 aspect-square transition-colors duration-200 !p-1.5"
|
||||
bg="bg-transparent"
|
||||
hover_bg={get_selectable_color_classes(false, {
|
||||
bg: true
|
||||
})}
|
||||
active_bg={get_selectable_color_classes(false, {
|
||||
bg: true
|
||||
})}
|
||||
click_function={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<RefreshCcwDot class="size-full" />
|
||||
</Button>
|
||||
{/if}
|
||||
<div
|
||||
class="w-14 content-center text-center select-none text-xs whitespace-nowrap"
|
||||
title={get_created_string(file.date_created, true)}
|
||||
>
|
||||
{get_created_string(file.date_created)}
|
||||
</div>
|
||||
<div
|
||||
class="h-[70%] border {get_grayed_out_border_color_strings(
|
||||
is_selected(file.id, $selected_file_ids)
|
||||
)} duration-200 transition-colors my-1"
|
||||
></div>
|
||||
<div
|
||||
class="w-12 content-center text-center select-none text-xs whitespace-nowrap truncate"
|
||||
title={file.type}
|
||||
>
|
||||
{is_folder ? 'Ordner' : (get_file_type(file)?.display_name ?? '?')}
|
||||
</div>
|
||||
<div
|
||||
class="h-[70%] border {get_grayed_out_border_color_strings(
|
||||
is_selected(file.id, $selected_file_ids)
|
||||
)} duration-200 transition-colors"
|
||||
></div>
|
||||
<div class="w-12 content-center text-center select-none text-xs whitespace-nowrap">
|
||||
{file.size}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import { ChevronRight, House } from 'lucide-svelte';
|
||||
import Button from './Button.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import type { MenuOption } from '../ts/types';
|
||||
import { change_file_path, current_file_path } from '../ts/stores/files';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
|
||||
let { bg = 'bg-stone-700' } = $props<{
|
||||
bg?: string;
|
||||
}>();
|
||||
|
||||
let outside_container: HTMLDivElement;
|
||||
let inside_container: HTMLDivElement;
|
||||
let resize_observer: ResizeObserver;
|
||||
let w10_div: HTMLDivElement;
|
||||
|
||||
let cut_folders: number = $state(0);
|
||||
// let folders_length: number = $state(get_folders($current_file_path).length);
|
||||
// let all_folders_length: number = $state(get_folders($current_file_path).length);
|
||||
|
||||
function get_folders(path: string): string[] {
|
||||
path = path.slice(1); // Cut first '/'
|
||||
if (path === '') return [];
|
||||
if (path.endsWith('/')) {
|
||||
path = path.slice(0, path.length - 1);
|
||||
}
|
||||
return path.split('/');
|
||||
}
|
||||
|
||||
function get_sliced_folders(path: string, cut_folders: number): string[] {
|
||||
const folders = get_folders(path);
|
||||
let sliced_folders: string[] = [];
|
||||
if (cut_folders !== get_folders($current_file_path).length)
|
||||
sliced_folders = folders.slice(cut_folders);
|
||||
return sliced_folders;
|
||||
}
|
||||
|
||||
function get_hidden_folders(path: string, cut_folders: number): string[] {
|
||||
const folders = get_folders(path);
|
||||
let hidden_folders: string[] = [];
|
||||
if (cut_folders !== get_folders($current_file_path).length)
|
||||
hidden_folders = folders.slice(0, cut_folders);
|
||||
return hidden_folders;
|
||||
}
|
||||
|
||||
function recalc() {
|
||||
if (!outside_container || !inside_container || !w10_div) return;
|
||||
const first_shrink = cut_folders === 0;
|
||||
const second_last_grow = cut_folders === 2;
|
||||
|
||||
const difference = outside_container.offsetWidth - inside_container.offsetWidth;
|
||||
|
||||
const w10_px = parseFloat(getComputedStyle(w10_div).width);
|
||||
|
||||
if ((!first_shrink && difference < 2 * w10_px) || (first_shrink && difference < w10_px)) {
|
||||
if (cut_folders < get_folders($current_file_path).length) {
|
||||
cut_folders += first_shrink ? 2 : 1;
|
||||
}
|
||||
} else if (
|
||||
(!second_last_grow && difference > 6 * w10_px) ||
|
||||
(second_last_grow && difference > 7 * w10_px)
|
||||
) {
|
||||
if (cut_folders >= 1) {
|
||||
if (second_last_grow) {
|
||||
cut_folders -= 2;
|
||||
} else {
|
||||
cut_folders -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get_hidden_menu_options(hidden_folders: string[], path: string): MenuOption[] {
|
||||
const out: MenuOption[] = [];
|
||||
for (let i = 0; i < hidden_folders.length; i++) {
|
||||
out.push({
|
||||
name: ' '.repeat(i) + hidden_folders[i],
|
||||
class: 'truncate max-w-80',
|
||||
on_select: () => {
|
||||
open_path(i + 1, path);
|
||||
}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function open_path(index_of_all_folders: number, path: string) {
|
||||
let new_path = '/';
|
||||
const all_folders = get_folders(path);
|
||||
for (let i = 0; i < index_of_all_folders; i++) {
|
||||
new_path += all_folders[i] + '/';
|
||||
}
|
||||
change_file_path(new_path);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
resize_observer = new ResizeObserver(() => recalc());
|
||||
resize_observer.observe(outside_container);
|
||||
resize_observer.observe(inside_container);
|
||||
// initial
|
||||
setTimeout(recalc, 0);
|
||||
});
|
||||
onDestroy(() => resize_observer?.disconnect());
|
||||
</script>
|
||||
|
||||
<div class="{bg} rounded-xl flex" bind:this={outside_container}>
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-row">
|
||||
<div bind:this={w10_div} class="flex">
|
||||
<Button
|
||||
className="py-1 shrink-0 grow-0 w-10"
|
||||
{bg}
|
||||
click_function={(e) => {
|
||||
open_path(0, $current_file_path);
|
||||
}}
|
||||
>
|
||||
<House class="size-full" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if cut_folders !== 0}
|
||||
<Button
|
||||
className="pl-0 py-1 grow"
|
||||
{bg}
|
||||
menu_options={get_hidden_menu_options(
|
||||
get_hidden_folders($current_file_path, cut_folders),
|
||||
$current_file_path
|
||||
)}
|
||||
menu_class="left-0"
|
||||
>
|
||||
<ChevronRight class="shrink-0 text-stone-500 h-full" />
|
||||
<span class="font-bold" title="Weiteren Pfad zeigen"> ... </span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row" bind:this={inside_container}>
|
||||
{#each get_sliced_folders($current_file_path, cut_folders) as folder, i (i)}
|
||||
<div animate:flip={{ duration: 100, easing: cubicOut }}>
|
||||
<Button
|
||||
className="shrink-0 py-1 pl-0 {i ===
|
||||
get_sliced_folders($current_file_path, cut_folders).length - 1
|
||||
? 'max-w-80 font-bold'
|
||||
: 'max-w-30'}"
|
||||
{bg}
|
||||
click_function={(e) => {
|
||||
open_path(cut_folders + i + 1, $current_file_path);
|
||||
}}
|
||||
>
|
||||
<ChevronRight class="shrink-0 text-stone-500 h-full" />
|
||||
<span class="truncate" title={folder}>
|
||||
{folder}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
let {className = ''} = $props<{className?: string}>();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="{className} lucide lucide-refresh-ccw-dot-icon lucide-refresh-ccw-dot"
|
||||
>
|
||||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
||||
<path d="M3 3v5h5" />
|
||||
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
|
||||
<path d="M16 16h5v5" />
|
||||
<path
|
||||
d="M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z"
|
||||
transform="translate(0,2.0)"
|
||||
/>
|
||||
</svg>
|
||||
Reference in New Issue
Block a user