@brftech/filex-core 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ArchiveViewer-BRFvhWwL.js → ArchiveViewer-ChemVSr_.js} +2 -2
- package/dist/{ArchiveViewer-BRFvhWwL.js.map → ArchiveViewer-ChemVSr_.js.map} +1 -1
- package/dist/{CsvViewer-1cGGKeGi.js → CsvViewer-BPLL4a6K.js} +2 -2
- package/dist/{CsvViewer-1cGGKeGi.js.map → CsvViewer-BPLL4a6K.js.map} +1 -1
- package/dist/{DrawioViewer-C0oThT59.js → DrawioViewer-HUDLaBMb.js} +2 -2
- package/dist/{DrawioViewer-C0oThT59.js.map → DrawioViewer-HUDLaBMb.js.map} +1 -1
- package/dist/{EpubViewer-DCPOD2S-.js → EpubViewer-Dlb9JwZx.js} +2 -2
- package/dist/{EpubViewer-DCPOD2S-.js.map → EpubViewer-Dlb9JwZx.js.map} +1 -1
- package/dist/{IpynbViewer-mv4xiv3H.js → IpynbViewer-omprV92r.js} +2 -2
- package/dist/{IpynbViewer-mv4xiv3H.js.map → IpynbViewer-omprV92r.js.map} +1 -1
- package/dist/{MermaidViewer-BBNVghTz.js → MermaidViewer-DwgszRcQ.js} +2 -2
- package/dist/{MermaidViewer-BBNVghTz.js.map → MermaidViewer-DwgszRcQ.js.map} +1 -1
- package/dist/{PsdViewer-B8ygWw-s.js → PsdViewer-DIHfkYmM.js} +2 -2
- package/dist/{PsdViewer-B8ygWw-s.js.map → PsdViewer-DIHfkYmM.js.map} +1 -1
- package/dist/{TiffViewer-BhHjf3s5.js → TiffViewer-D5YH_ISS.js} +2 -2
- package/dist/{TiffViewer-BhHjf3s5.js.map → TiffViewer-D5YH_ISS.js.map} +1 -1
- package/dist/{Viewer3D-BhOJpzyG.js → Viewer3D-Qq7TBKJ_.js} +2 -2
- package/dist/{Viewer3D-BhOJpzyG.js.map → Viewer3D-Qq7TBKJ_.js.map} +1 -1
- package/dist/filex-core.js +1 -1
- package/dist/filex-core.umd.cjs +38 -38
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-BjiUhk9K.js → index-BekR3mux.js} +4852 -4793
- package/dist/index-BekR3mux.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/FileExplorer.vue +59 -5
- package/src/components/SecondaryPane.vue +2 -2
- package/src/composables/useKeyboardShortcuts.ts +3 -0
- package/src/lib/listing.ts +48 -0
- package/src/locales/en.ts +4 -0
- package/src/locales/tr.ts +4 -0
- package/dist/index-BjiUhk9K.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1472,6 +1472,7 @@ export declare interface ShortcutHandlers {
|
|
|
1472
1472
|
onGoUp?: () => void;
|
|
1473
1473
|
onShowHelp?: () => void;
|
|
1474
1474
|
onToggleInspector?: () => void;
|
|
1475
|
+
onToggleHidden?: () => void;
|
|
1475
1476
|
onQuickLook?: () => void;
|
|
1476
1477
|
onTabNew?: () => void;
|
|
1477
1478
|
onTabClose?: () => void;
|
package/package.json
CHANGED
package/src/FileExplorer.vue
CHANGED
|
@@ -90,7 +90,9 @@ import {
|
|
|
90
90
|
internal-entry filter + virtual `.trash` row must be identical in both
|
|
91
91
|
panes or split view shows mismatched rows). */
|
|
92
92
|
import {
|
|
93
|
-
|
|
93
|
+
filterListing,
|
|
94
|
+
showHiddenFiles,
|
|
95
|
+
setShowHiddenFiles,
|
|
94
96
|
injectTrashRow,
|
|
95
97
|
hydrateTrashRow as hydrateTrashRowShared,
|
|
96
98
|
} from './lib/listing';
|
|
@@ -274,12 +276,26 @@ const trashMode = ref(false);
|
|
|
274
276
|
const trashOrigin = ref<string>('');
|
|
275
277
|
const trashActive = computed(() => trashMode.value);
|
|
276
278
|
|
|
279
|
+
// When the caller can see exactly ONE storage, the multi-storage root is a
|
|
280
|
+
// one-row list that carries no information — the user clicks through it every
|
|
281
|
+
// single time. Treat that storage as the floor instead: open it directly and
|
|
282
|
+
// stop offering an "up" that only leads back to the one row.
|
|
283
|
+
//
|
|
284
|
+
// Empty string means "not in that situation" (single-storage mode, or more
|
|
285
|
+
// than one storage visible), which leaves every existing path untouched.
|
|
286
|
+
const soleStorageName = computed(() => {
|
|
287
|
+
if (!multiStorageRoot.value) return '';
|
|
288
|
+
const list = props.config.storages ?? [];
|
|
289
|
+
return list.length === 1 ? list[0].name : '';
|
|
290
|
+
});
|
|
291
|
+
|
|
277
292
|
// canGoUp/goUp — toolbar's "↑ Up one level" button. In single-storage
|
|
278
293
|
// mode "" means the storage root; in multi-storage mode "" means
|
|
279
294
|
// the global root (storage list). Both → no parent → button hidden.
|
|
280
295
|
const canGoUp = computed(() => {
|
|
281
296
|
const p = (currentPath.value ?? '').replace(/^\/+|\/+$/g, '');
|
|
282
297
|
if (rootFloor && p === rootFloor) return false; // at the confined floor — nowhere above
|
|
298
|
+
if (soleStorageName.value && p === soleStorageName.value) return false;
|
|
283
299
|
return p.length > 0;
|
|
284
300
|
});
|
|
285
301
|
|
|
@@ -300,6 +316,8 @@ function goUp() {
|
|
|
300
316
|
}
|
|
301
317
|
const cur = (currentPath.value ?? '').replace(/^\/+|\/+$/g, '');
|
|
302
318
|
if (!cur || cur === rootFloor) return;
|
|
319
|
+
// The button is hidden here, but Alt+↑ / Backspace still route through.
|
|
320
|
+
if (soleStorageName.value && cur === soleStorageName.value) return;
|
|
303
321
|
const idx = cur.lastIndexOf('/');
|
|
304
322
|
let parent = idx === -1 ? '' : cur.slice(0, idx);
|
|
305
323
|
// Never step above the confined floor.
|
|
@@ -674,6 +692,16 @@ function virtualStorageRows(): FileNode[] {
|
|
|
674
692
|
} as unknown as FileNode));
|
|
675
693
|
}
|
|
676
694
|
|
|
695
|
+
// Flip dot-file visibility. Both panes filter at load time rather than in a
|
|
696
|
+
// computed, so the listings are re-fetched instead of re-filtered — that keeps
|
|
697
|
+
// selection and the operations that read `files` working on exactly what is
|
|
698
|
+
// on screen.
|
|
699
|
+
function toggleHiddenFiles() {
|
|
700
|
+
setShowHiddenFiles(!showHiddenFiles.value);
|
|
701
|
+
void load();
|
|
702
|
+
void splitPaneRef.value?.reload();
|
|
703
|
+
}
|
|
704
|
+
|
|
677
705
|
async function load(path?: string) {
|
|
678
706
|
loading.value = true;
|
|
679
707
|
// Any normal navigation exits trash mode (the trash view is entered only
|
|
@@ -694,6 +722,10 @@ async function load(path?: string) {
|
|
|
694
722
|
// Multi-storage virtual root — synthesize a list of storages
|
|
695
723
|
// instead of calling the backend.
|
|
696
724
|
if (multiStorageRoot.value && !virtualToWire(requested)) {
|
|
725
|
+
// One visible storage → skip the one-row list and open it. Recursion is
|
|
726
|
+
// bounded: the recursive call carries a non-empty path, so
|
|
727
|
+
// virtualToWire() resolves and this branch is not re-entered.
|
|
728
|
+
if (soleStorageName.value) return await load(soleStorageName.value);
|
|
697
729
|
currentPath.value = '';
|
|
698
730
|
adapter.value = '';
|
|
699
731
|
dirname.value = '';
|
|
@@ -716,7 +748,7 @@ async function load(path?: string) {
|
|
|
716
748
|
subtree; '' resets on every plain folder. Drives the lock screen. */
|
|
717
749
|
e2eRoot.value = typeof resp.e2e_root === 'string' ? resp.e2e_root : '';
|
|
718
750
|
/* /wiring:e2 */
|
|
719
|
-
files.value =
|
|
751
|
+
files.value = filterListing(resp.files);
|
|
720
752
|
// Inject virtual `.trash` entry at root only — shared helper so the
|
|
721
753
|
// split-view secondary pane shows the exact same row (no row-offset).
|
|
722
754
|
if (injectTrashRow(files.value, resp.adapter, resp.dirname, props.config.trashVisible !== false)) {
|
|
@@ -1147,6 +1179,7 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1147
1179
|
},
|
|
1148
1180
|
/* /cila:c wiring */
|
|
1149
1181
|
onQuickLook: () => quickLookToggle() /* wiring:c2 */,
|
|
1182
|
+
onToggleHidden: () => toggleHiddenFiles(),
|
|
1150
1183
|
onToggleInspector: () => toggleInspector() /* koru:k1 */,
|
|
1151
1184
|
/* wiring:d1 — sekme aksiyonları (registry: tab-new/close/next/prev) */
|
|
1152
1185
|
onTabNew: () => newTabHere(),
|
|
@@ -1463,10 +1496,20 @@ const contextActions = computed<ContextAction[]>(() => {
|
|
|
1463
1496
|
// Empty background right-click: folder-level actions only. Viewers (no edit
|
|
1464
1497
|
// on this dir) get nothing here.
|
|
1465
1498
|
if (!any) {
|
|
1466
|
-
|
|
1499
|
+
// Showing hidden files is a VIEW preference, so it stays available to
|
|
1500
|
+
// viewers too — the edit gate below only covers the mutating actions.
|
|
1501
|
+
const view: ContextAction[] = [
|
|
1502
|
+
{
|
|
1503
|
+
key: 'toggle-hidden',
|
|
1504
|
+
label: showHiddenFiles.value ? t('ctx.hide_hidden') : t('ctx.show_hidden'),
|
|
1505
|
+
icon: showHiddenFiles.value ? '🙈' : '👁',
|
|
1506
|
+
},
|
|
1507
|
+
];
|
|
1508
|
+
if (!permCanEdit(dirPerm.value)) return view;
|
|
1467
1509
|
return [
|
|
1468
1510
|
{ key: 'new-folder', label: t('toolbar.new_folder'), icon: '📁' },
|
|
1469
1511
|
{ key: 'paste', label: t('ctx.paste'), icon: '📋', disabled: !clipboard.value.mode },
|
|
1512
|
+
...view,
|
|
1470
1513
|
];
|
|
1471
1514
|
}
|
|
1472
1515
|
|
|
@@ -1633,6 +1676,9 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
|
|
|
1633
1676
|
case 'restore':
|
|
1634
1677
|
if (targets.length > 0) await restoreSelection(targets);
|
|
1635
1678
|
break;
|
|
1679
|
+
case 'toggle-hidden':
|
|
1680
|
+
toggleHiddenFiles();
|
|
1681
|
+
break;
|
|
1636
1682
|
case 'new-folder':
|
|
1637
1683
|
mutationInPane.value = ctxMode.value === 'pane'; /* ui-fix */
|
|
1638
1684
|
showNewFolder.value = true;
|
|
@@ -1921,9 +1967,16 @@ async function legacyUpload(file: File) {
|
|
|
1921
1967
|
patch({ percent: 100, uploadedBytes: file.size, status: 'done' });
|
|
1922
1968
|
emit('upload-progress', { uploadId: id, percent: 100, done: true });
|
|
1923
1969
|
} catch (err) {
|
|
1924
|
-
|
|
1970
|
+
// Tell the USER, not just the embedding app. `emit('error')` alone left a
|
|
1971
|
+
// standalone deployment silent: the progress bar ran to 100% (the bytes
|
|
1972
|
+
// do go out — the server rejects them afterwards), the row flipped to an
|
|
1973
|
+
// error state carrying no message, and nothing else appeared. olivov lost
|
|
1974
|
+
// ten days of uploads to that silence (H2, 2026-08-05).
|
|
1975
|
+
const message = (err as Error).message;
|
|
1976
|
+
patch({ status: 'error', error: message });
|
|
1977
|
+
flashToast(t('upload.failed', { name: file.name }));
|
|
1925
1978
|
emit('error', {
|
|
1926
|
-
message
|
|
1979
|
+
message,
|
|
1927
1980
|
context: { op: 'upload', file: file.name },
|
|
1928
1981
|
});
|
|
1929
1982
|
}
|
|
@@ -2390,6 +2443,7 @@ function retryUploadJob(job: UploadJob) {
|
|
|
2390
2443
|
})
|
|
2391
2444
|
.catch((err: Error) => {
|
|
2392
2445
|
patchRetry({ status: 'error', error: err.message });
|
|
2446
|
+
flashToast(t('upload.failed', { name: file.name }));
|
|
2393
2447
|
emit('error', { message: err.message, context: { op: 'upload-retry', file: file.name } });
|
|
2394
2448
|
});
|
|
2395
2449
|
}
|
|
@@ -21,7 +21,7 @@ import type { FileApi } from '../composables/useFileApi';
|
|
|
21
21
|
import type { FileNode, ViewMode } from '../types/FileNode';
|
|
22
22
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
23
23
|
import { useLocale } from '../composables/useLocale';
|
|
24
|
-
import {
|
|
24
|
+
import { filterListing, injectTrashRow, hydrateTrashRow } from '../lib/listing';
|
|
25
25
|
import ListView from './ListView.vue';
|
|
26
26
|
import GridView from './GridView.vue';
|
|
27
27
|
import GalleryView from './GalleryView.vue';
|
|
@@ -110,7 +110,7 @@ async function loadPane(target?: string): Promise<void> {
|
|
|
110
110
|
const resp = await props.api.index(props.qualify(requested));
|
|
111
111
|
// Same internal-entry filter + virtual `.trash` row as the main panel
|
|
112
112
|
// (shared helpers → both split panes list identical rows, no offset).
|
|
113
|
-
files.value =
|
|
113
|
+
files.value = filterListing(resp.files);
|
|
114
114
|
if (injectTrashRow(files.value, resp.adapter, resp.dirname, props.trashVisible !== false)) {
|
|
115
115
|
void hydrateTrashRow(files.value, resp.adapter, props.api);
|
|
116
116
|
}
|
|
@@ -37,6 +37,7 @@ export interface ShortcutHandlers {
|
|
|
37
37
|
onGoUp?: () => void; // Alt+Up / Backspace (when nothing selected)
|
|
38
38
|
onShowHelp?: () => void; // ? (Shift+/ on most layouts)
|
|
39
39
|
onToggleInspector?: () => void; // i (koru:k1 details panel)
|
|
40
|
+
onToggleHidden?: () => void; // Ctrl+Shift+. — dot-file visibility
|
|
40
41
|
onQuickLook?: () => void; // Space (wiring:c2 quick-look overlay)
|
|
41
42
|
/* wiring:d1 — tab strip actions */
|
|
42
43
|
onTabNew?: () => void; // Ctrl+T
|
|
@@ -85,6 +86,7 @@ export const SHORTCUT_ACTIONS: ShortcutActionDef[] = [
|
|
|
85
86
|
{ id: 'close', defaultCombo: 'Esc', customizable: false, prevent: false, inForms: true, labelKey: 'shortcuts.close', groupKey: 'shortcuts.group.nav' },
|
|
86
87
|
{ id: 'inspector', defaultCombo: 'I', labelKey: 'shortcuts.inspector', groupKey: 'shortcuts.group.nav' } /* koru:k1 */,
|
|
87
88
|
{ id: 'help', defaultCombo: '?', labelKey: 'shortcuts.help', groupKey: 'shortcuts.group.nav' },
|
|
89
|
+
{ id: 'toggle-hidden', defaultCombo: 'Ctrl+Shift+.', labelKey: 'shortcuts.toggle_hidden', groupKey: 'shortcuts.group.nav' },
|
|
88
90
|
// Selection
|
|
89
91
|
{ id: 'select-all', defaultCombo: 'Ctrl+A', labelKey: 'shortcuts.select_all', groupKey: 'shortcuts.group.selection' },
|
|
90
92
|
// File operations
|
|
@@ -112,6 +114,7 @@ const HANDLER_KEY: Record<string, keyof ShortcutHandlers> = {
|
|
|
112
114
|
quicklook: 'onQuickLook',
|
|
113
115
|
close: 'onClose',
|
|
114
116
|
inspector: 'onToggleInspector',
|
|
117
|
+
'toggle-hidden': 'onToggleHidden',
|
|
115
118
|
help: 'onShowHelp',
|
|
116
119
|
'select-all': 'onSelectAll',
|
|
117
120
|
rename: 'onRename',
|
package/src/lib/listing.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* must be identical in both, otherwise split view shows mismatched rows
|
|
7
7
|
* (the trash row missing on one side → visible row-offset).
|
|
8
8
|
*/
|
|
9
|
+
import { ref } from 'vue';
|
|
10
|
+
|
|
9
11
|
import type { FileNode } from '../types/FileNode';
|
|
10
12
|
import { E2E_MARKER_NAME } from './e2ecrypto';
|
|
11
13
|
|
|
@@ -31,6 +33,52 @@ export function filterInternalEntries(files: FileNode[]): FileNode[] {
|
|
|
31
33
|
});
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
const SHOW_HIDDEN_LS_KEY = 'filex.showHidden';
|
|
37
|
+
|
|
38
|
+
function readShowHidden(): boolean {
|
|
39
|
+
try {
|
|
40
|
+
return localStorage.getItem(SHOW_HIDDEN_LS_KEY) === '1';
|
|
41
|
+
} catch {
|
|
42
|
+
return false; // private mode / no storage → hidden, the safe default
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether dot-prefixed entries are listed. Off by default, like every other
|
|
48
|
+
* file manager. Shared by both panes so split view can't disagree with itself.
|
|
49
|
+
*/
|
|
50
|
+
export const showHiddenFiles = ref(readShowHidden());
|
|
51
|
+
|
|
52
|
+
export function setShowHiddenFiles(v: boolean): void {
|
|
53
|
+
showHiddenFiles.value = v;
|
|
54
|
+
try {
|
|
55
|
+
localStorage.setItem(SHOW_HIDDEN_LS_KEY, v ? '1' : '0');
|
|
56
|
+
} catch {
|
|
57
|
+
/* preference just won't persist */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Hide dot-prefixed entries unless the user asked to see them.
|
|
63
|
+
*
|
|
64
|
+
* These are mostly not the user's files at all: a Mac writing over WebDAV
|
|
65
|
+
* leaves a `.DS_Store` in every folder it opens and an AppleDouble `._name`
|
|
66
|
+
* beside every file carrying extended attributes. Finder hides its own litter
|
|
67
|
+
* locally, so seeing it reappear in the web UI reads as corruption.
|
|
68
|
+
*
|
|
69
|
+
* A toggle rather than a blocklist, because they ARE real files: hiding them
|
|
70
|
+
* outright would leave the ones already uploaded unreachable and undeletable.
|
|
71
|
+
*/
|
|
72
|
+
export function filterHiddenEntries(files: FileNode[], showHidden: boolean): FileNode[] {
|
|
73
|
+
if (showHidden) return files || [];
|
|
74
|
+
return (files || []).filter((f) => !(f.basename || '').startsWith('.'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** filterInternalEntries + filterHiddenEntries — what both panes render. */
|
|
78
|
+
export function filterListing(files: FileNode[]): FileNode[] {
|
|
79
|
+
return filterHiddenEntries(filterInternalEntries(files), showHiddenFiles.value);
|
|
80
|
+
}
|
|
81
|
+
|
|
34
82
|
/** True when `dirname` (adapter-qualified) is the storage root, where the
|
|
35
83
|
* virtual trash row belongs. */
|
|
36
84
|
export function isStorageRootDir(dirname: string): boolean {
|
package/src/locales/en.ts
CHANGED
|
@@ -21,6 +21,8 @@ export const en: Record<string, string> = {
|
|
|
21
21
|
'ctx.move': 'Move',
|
|
22
22
|
'ctx.copy': 'Copy',
|
|
23
23
|
'ctx.cut': 'Cut',
|
|
24
|
+
'ctx.show_hidden': 'Show hidden files',
|
|
25
|
+
'ctx.hide_hidden': 'Hide hidden files',
|
|
24
26
|
'ctx.paste': 'Paste',
|
|
25
27
|
'ctx.info': 'Info',
|
|
26
28
|
'ctx.duplicate': 'Duplicate',
|
|
@@ -54,6 +56,7 @@ export const en: Record<string, string> = {
|
|
|
54
56
|
'upload.progress': 'Uploading',
|
|
55
57
|
'upload.done': 'Done',
|
|
56
58
|
'upload.error': 'Error',
|
|
59
|
+
'upload.failed': 'Could not upload “{name}”',
|
|
57
60
|
'upload.aborted': 'Aborted',
|
|
58
61
|
'upload.cancel': 'Cancel',
|
|
59
62
|
|
|
@@ -162,6 +165,7 @@ export const en: Record<string, string> = {
|
|
|
162
165
|
'shortcuts.go_up': 'Go up one level',
|
|
163
166
|
'shortcuts.open': 'Open selected item',
|
|
164
167
|
'shortcuts.close': 'Close / cancel',
|
|
168
|
+
'shortcuts.toggle_hidden': 'Show/hide hidden files',
|
|
165
169
|
'shortcuts.help': 'This help dialog',
|
|
166
170
|
'shortcuts.select_all': 'Select all',
|
|
167
171
|
'shortcuts.rename': 'Rename',
|
package/src/locales/tr.ts
CHANGED
|
@@ -21,6 +21,8 @@ export const tr: Record<string, string> = {
|
|
|
21
21
|
'ctx.move': 'Taşı',
|
|
22
22
|
'ctx.copy': 'Kopyala',
|
|
23
23
|
'ctx.cut': 'Kes',
|
|
24
|
+
'ctx.show_hidden': 'Gizli dosyaları göster',
|
|
25
|
+
'ctx.hide_hidden': 'Gizli dosyaları gizle',
|
|
24
26
|
'ctx.paste': 'Yapıştır',
|
|
25
27
|
'ctx.info': 'Bilgi',
|
|
26
28
|
'ctx.duplicate': 'Kopyasını Oluştur',
|
|
@@ -54,6 +56,7 @@ export const tr: Record<string, string> = {
|
|
|
54
56
|
'upload.progress': 'Yükleniyor',
|
|
55
57
|
'upload.done': 'Tamamlandı',
|
|
56
58
|
'upload.error': 'Hata',
|
|
59
|
+
'upload.failed': '“{name}” yüklenemedi',
|
|
57
60
|
'upload.aborted': 'İptal edildi',
|
|
58
61
|
'upload.cancel': 'İptal',
|
|
59
62
|
|
|
@@ -162,6 +165,7 @@ export const tr: Record<string, string> = {
|
|
|
162
165
|
'shortcuts.go_up': 'Üst klasöre çık',
|
|
163
166
|
'shortcuts.open': 'Seçili öğeyi aç',
|
|
164
167
|
'shortcuts.close': 'Kapat / vazgeç',
|
|
168
|
+
'shortcuts.toggle_hidden': 'Gizli dosyaları göster/gizle',
|
|
165
169
|
'shortcuts.help': 'Bu yardım penceresi',
|
|
166
170
|
'shortcuts.select_all': 'Tümünü seç',
|
|
167
171
|
'shortcuts.rename': 'Yeniden adlandır',
|