diff options
Diffstat (limited to 'apps/files')
97 files changed, 921 insertions, 197 deletions
diff --git a/apps/files/l10n/ar.js b/apps/files/l10n/ar.js index beb1e0eb1cc..ffd13adc0d5 100644 --- a/apps/files/l10n/ar.js +++ b/apps/files/l10n/ar.js @@ -224,6 +224,7 @@ OC.L10N.register( "Show those shortcuts" : "أعرُض تلك الاختصارات", "You" : "أنت", "Shared multiple times with different people" : "تمّت مشاركته عدة مرات مع أشخاص متعددين", + "Unable to change the favorite state of the file" : "تعذّر تغيير حالة المُفضَّلة للملف", "Error while loading the file data" : "خطأ اثناء تحميل بيانات الملف", "Owner" : "المالك", "Remove from favorites" : "إزالة من المفضلة", diff --git a/apps/files/l10n/ar.json b/apps/files/l10n/ar.json index b7872f2405d..19dc6eaad41 100644 --- a/apps/files/l10n/ar.json +++ b/apps/files/l10n/ar.json @@ -222,6 +222,7 @@ "Show those shortcuts" : "أعرُض تلك الاختصارات", "You" : "أنت", "Shared multiple times with different people" : "تمّت مشاركته عدة مرات مع أشخاص متعددين", + "Unable to change the favorite state of the file" : "تعذّر تغيير حالة المُفضَّلة للملف", "Error while loading the file data" : "خطأ اثناء تحميل بيانات الملف", "Owner" : "المالك", "Remove from favorites" : "إزالة من المفضلة", diff --git a/apps/files/l10n/ca.js b/apps/files/l10n/ca.js index cc3cff9aac8..c087434a9d2 100644 --- a/apps/files/l10n/ca.js +++ b/apps/files/l10n/ca.js @@ -218,6 +218,7 @@ OC.L10N.register( "Show those shortcuts" : "Mostra aquestes dreceres", "You" : "Vós", "Shared multiple times with different people" : "S'ha compartit diverses vegades amb persones diferents", + "Unable to change the favorite state of the file" : "No es pot canviar l'estat preferit del fitxer", "Error while loading the file data" : "S'ha produït un error en carregar la informació del fitxer", "Owner" : "Propietat", "Remove from favorites" : "Suprimeix dels preferits", diff --git a/apps/files/l10n/ca.json b/apps/files/l10n/ca.json index e0304838a3e..1990d885d0b 100644 --- a/apps/files/l10n/ca.json +++ b/apps/files/l10n/ca.json @@ -216,6 +216,7 @@ "Show those shortcuts" : "Mostra aquestes dreceres", "You" : "Vós", "Shared multiple times with different people" : "S'ha compartit diverses vegades amb persones diferents", + "Unable to change the favorite state of the file" : "No es pot canviar l'estat preferit del fitxer", "Error while loading the file data" : "S'ha produït un error en carregar la informació del fitxer", "Owner" : "Propietat", "Remove from favorites" : "Suprimeix dels preferits", diff --git a/apps/files/l10n/cs.js b/apps/files/l10n/cs.js index 8c82398bd13..43010512ed6 100644 --- a/apps/files/l10n/cs.js +++ b/apps/files/l10n/cs.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Zobrazit tyto zkratky", "You" : "Vy", "Shared multiple times with different people" : "Nasdílet několikrát různým lidem", + "Unable to change the favorite state of the file" : "Nebylo možné změnit stav zařazení mezi oblíbené souboru", "Error while loading the file data" : "Chyba při načítání dat souboru", "Owner" : "Vlastník", "Remove from favorites" : "Odebrat z oblíbených", diff --git a/apps/files/l10n/cs.json b/apps/files/l10n/cs.json index 5c5eb759467..20cfeaa25a0 100644 --- a/apps/files/l10n/cs.json +++ b/apps/files/l10n/cs.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Zobrazit tyto zkratky", "You" : "Vy", "Shared multiple times with different people" : "Nasdílet několikrát různým lidem", + "Unable to change the favorite state of the file" : "Nebylo možné změnit stav zařazení mezi oblíbené souboru", "Error while loading the file data" : "Chyba při načítání dat souboru", "Owner" : "Vlastník", "Remove from favorites" : "Odebrat z oblíbených", diff --git a/apps/files/l10n/da.js b/apps/files/l10n/da.js index 9dbfb1d372a..4a538d91e3f 100644 --- a/apps/files/l10n/da.js +++ b/apps/files/l10n/da.js @@ -218,6 +218,7 @@ OC.L10N.register( "Show those shortcuts" : "Vis disse genveje", "You" : "Dig", "Shared multiple times with different people" : "Delt flere gange med forskellige mennesker", + "Unable to change the favorite state of the file" : "Kan ikke ændre favoritstatus for filen", "Error while loading the file data" : "Fejl under indlæsning af fildata", "Owner" : "Ejer", "Remove from favorites" : "Fjern fra favoritter", diff --git a/apps/files/l10n/da.json b/apps/files/l10n/da.json index 0aa2c1be278..95cd920e0a0 100644 --- a/apps/files/l10n/da.json +++ b/apps/files/l10n/da.json @@ -216,6 +216,7 @@ "Show those shortcuts" : "Vis disse genveje", "You" : "Dig", "Shared multiple times with different people" : "Delt flere gange med forskellige mennesker", + "Unable to change the favorite state of the file" : "Kan ikke ændre favoritstatus for filen", "Error while loading the file data" : "Fejl under indlæsning af fildata", "Owner" : "Ejer", "Remove from favorites" : "Fjern fra favoritter", diff --git a/apps/files/l10n/de.js b/apps/files/l10n/de.js index 599ce782ec8..80d45307fee 100644 --- a/apps/files/l10n/de.js +++ b/apps/files/l10n/de.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Diese Tastaturkürzel anzeigen", "You" : "Du", "Shared multiple times with different people" : "Mehrmals mit verschiedenen Personen geteilt", + "Unable to change the favorite state of the file" : "Der Favoriten-Status der Datei kann nicht geändert werden", "Error while loading the file data" : "Fehler beim Laden der Datei-Daten", "Owner" : "Besitzer", "Remove from favorites" : "Aus Favoriten entfernen", diff --git a/apps/files/l10n/de.json b/apps/files/l10n/de.json index f131177909e..571b3a02f62 100644 --- a/apps/files/l10n/de.json +++ b/apps/files/l10n/de.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Diese Tastaturkürzel anzeigen", "You" : "Du", "Shared multiple times with different people" : "Mehrmals mit verschiedenen Personen geteilt", + "Unable to change the favorite state of the file" : "Der Favoriten-Status der Datei kann nicht geändert werden", "Error while loading the file data" : "Fehler beim Laden der Datei-Daten", "Owner" : "Besitzer", "Remove from favorites" : "Aus Favoriten entfernen", diff --git a/apps/files/l10n/de_DE.js b/apps/files/l10n/de_DE.js index 0fb3fd77045..2528fa26903 100644 --- a/apps/files/l10n/de_DE.js +++ b/apps/files/l10n/de_DE.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Diese Tastaturkürzel anzeigen", "You" : "Sie", "Shared multiple times with different people" : "Mehrmals mit verschiedenen Personen geteilt", + "Unable to change the favorite state of the file" : "Der Favoriten-Status der Datei kann nicht geändert werden", "Error while loading the file data" : "Fehler beim Laden der Datei-Daten", "Owner" : "Besitzer", "Remove from favorites" : "Von Favoriten entfernen", diff --git a/apps/files/l10n/de_DE.json b/apps/files/l10n/de_DE.json index 3169b3751c1..2a661823839 100644 --- a/apps/files/l10n/de_DE.json +++ b/apps/files/l10n/de_DE.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Diese Tastaturkürzel anzeigen", "You" : "Sie", "Shared multiple times with different people" : "Mehrmals mit verschiedenen Personen geteilt", + "Unable to change the favorite state of the file" : "Der Favoriten-Status der Datei kann nicht geändert werden", "Error while loading the file data" : "Fehler beim Laden der Datei-Daten", "Owner" : "Besitzer", "Remove from favorites" : "Von Favoriten entfernen", diff --git a/apps/files/l10n/en_GB.js b/apps/files/l10n/en_GB.js index c5a7999dfc3..651479c0ba3 100644 --- a/apps/files/l10n/en_GB.js +++ b/apps/files/l10n/en_GB.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Show those shortcuts", "You" : "You", "Shared multiple times with different people" : "Shared multiple times with different people", + "Unable to change the favorite state of the file" : "Unable to change the favorite state of the file", "Error while loading the file data" : "Error while loading the file data", "Owner" : "Owner", "Remove from favorites" : "Remove from favourites", diff --git a/apps/files/l10n/en_GB.json b/apps/files/l10n/en_GB.json index 2178bba0961..c0be990b162 100644 --- a/apps/files/l10n/en_GB.json +++ b/apps/files/l10n/en_GB.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Show those shortcuts", "You" : "You", "Shared multiple times with different people" : "Shared multiple times with different people", + "Unable to change the favorite state of the file" : "Unable to change the favorite state of the file", "Error while loading the file data" : "Error while loading the file data", "Owner" : "Owner", "Remove from favorites" : "Remove from favourites", diff --git a/apps/files/l10n/es.js b/apps/files/l10n/es.js index 9b294b47eb9..f719cf94b8f 100644 --- a/apps/files/l10n/es.js +++ b/apps/files/l10n/es.js @@ -220,6 +220,7 @@ OC.L10N.register( "Show those shortcuts" : "Mostrar estos atajos de teclado", "You" : "Usted", "Shared multiple times with different people" : "Compartido múltiples veces con diferentes personas", + "Unable to change the favorite state of the file" : "Deshacer los cambios del estado de favorito del archivo", "Error while loading the file data" : "Error al cargar los datos del archivo", "Owner" : "Dueño", "Remove from favorites" : "Quitar de favoritos", diff --git a/apps/files/l10n/es.json b/apps/files/l10n/es.json index 75439fa538a..3d6910f16fd 100644 --- a/apps/files/l10n/es.json +++ b/apps/files/l10n/es.json @@ -218,6 +218,7 @@ "Show those shortcuts" : "Mostrar estos atajos de teclado", "You" : "Usted", "Shared multiple times with different people" : "Compartido múltiples veces con diferentes personas", + "Unable to change the favorite state of the file" : "Deshacer los cambios del estado de favorito del archivo", "Error while loading the file data" : "Error al cargar los datos del archivo", "Owner" : "Dueño", "Remove from favorites" : "Quitar de favoritos", diff --git a/apps/files/l10n/et_EE.js b/apps/files/l10n/et_EE.js index 948cbb040ac..801501c12d3 100644 --- a/apps/files/l10n/et_EE.js +++ b/apps/files/l10n/et_EE.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Näita neid otseteid", "You" : "Sina", "Shared multiple times with different people" : "Jagatud mitu korda eri kasutajate poolt", + "Unable to change the favorite state of the file" : "Faili olekut lemmikuna ei õnnestu muuta", "Error while loading the file data" : "Viga faili andmete laadimisel", "Owner" : "Omanik", "Remove from favorites" : "Eemalda lemmikutest", @@ -271,6 +272,8 @@ OC.L10N.register( "Download" : "Laadi alla", "Moving \"{source}\" to \"{destination}\" …" : "Teisaldan „{source}“ → „{destination}“…", "Copying \"{source}\" to \"{destination}\" …" : "Kopeerin „{source}“ → „{destination}“…", + "Destination is not a folder" : "Sihtasukoht pole kaust", + "This file/folder is already in that directory" : "See fail või kaust juba asub selles kaustas", "You cannot move a file/folder onto itself or into a subfolder of itself" : "Sa ei saa faili või kausta iseendaks teisaldada ega teisaldada kausta iseenda alamkausta", "(copy)" : "(koopia)", "(copy %n)" : "(%n koopia)", diff --git a/apps/files/l10n/et_EE.json b/apps/files/l10n/et_EE.json index f5bbc600470..16ec2e6a3fd 100644 --- a/apps/files/l10n/et_EE.json +++ b/apps/files/l10n/et_EE.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Näita neid otseteid", "You" : "Sina", "Shared multiple times with different people" : "Jagatud mitu korda eri kasutajate poolt", + "Unable to change the favorite state of the file" : "Faili olekut lemmikuna ei õnnestu muuta", "Error while loading the file data" : "Viga faili andmete laadimisel", "Owner" : "Omanik", "Remove from favorites" : "Eemalda lemmikutest", @@ -269,6 +270,8 @@ "Download" : "Laadi alla", "Moving \"{source}\" to \"{destination}\" …" : "Teisaldan „{source}“ → „{destination}“…", "Copying \"{source}\" to \"{destination}\" …" : "Kopeerin „{source}“ → „{destination}“…", + "Destination is not a folder" : "Sihtasukoht pole kaust", + "This file/folder is already in that directory" : "See fail või kaust juba asub selles kaustas", "You cannot move a file/folder onto itself or into a subfolder of itself" : "Sa ei saa faili või kausta iseendaks teisaldada ega teisaldada kausta iseenda alamkausta", "(copy)" : "(koopia)", "(copy %n)" : "(%n koopia)", diff --git a/apps/files/l10n/fr.js b/apps/files/l10n/fr.js index bd9c1a89b0e..aa856f2e618 100644 --- a/apps/files/l10n/fr.js +++ b/apps/files/l10n/fr.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Montrer ces raccourcis", "You" : "Vous", "Shared multiple times with different people" : "Partagé plusieurs fois avec plusieurs personnes", + "Unable to change the favorite state of the file" : "Impossible de modifier l'état favori du fichier", "Error while loading the file data" : "Erreur lors du chargement du fichier de données", "Owner" : "Propriétaire", "Remove from favorites" : "Retirer des favoris", diff --git a/apps/files/l10n/fr.json b/apps/files/l10n/fr.json index 2d3504a41f9..73ecb0659bb 100644 --- a/apps/files/l10n/fr.json +++ b/apps/files/l10n/fr.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Montrer ces raccourcis", "You" : "Vous", "Shared multiple times with different people" : "Partagé plusieurs fois avec plusieurs personnes", + "Unable to change the favorite state of the file" : "Impossible de modifier l'état favori du fichier", "Error while loading the file data" : "Erreur lors du chargement du fichier de données", "Owner" : "Propriétaire", "Remove from favorites" : "Retirer des favoris", diff --git a/apps/files/l10n/ga.js b/apps/files/l10n/ga.js index dd836588106..cc7185656dd 100644 --- a/apps/files/l10n/ga.js +++ b/apps/files/l10n/ga.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Taispeáin na haicearraí sin", "You" : "tú", "Shared multiple times with different people" : "Roinnte go minic le daoine éagsúla", + "Unable to change the favorite state of the file" : "Ní féidir staid an chomhaid is fearr leat a athrú", "Error while loading the file data" : "Earráid agus sonraí an chomhaid á lódáil", "Owner" : "Úinéir", "Remove from favorites" : "Bain ó cheanáin", diff --git a/apps/files/l10n/ga.json b/apps/files/l10n/ga.json index 52735440088..b68edfb56c7 100644 --- a/apps/files/l10n/ga.json +++ b/apps/files/l10n/ga.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Taispeáin na haicearraí sin", "You" : "tú", "Shared multiple times with different people" : "Roinnte go minic le daoine éagsúla", + "Unable to change the favorite state of the file" : "Ní féidir staid an chomhaid is fearr leat a athrú", "Error while loading the file data" : "Earráid agus sonraí an chomhaid á lódáil", "Owner" : "Úinéir", "Remove from favorites" : "Bain ó cheanáin", diff --git a/apps/files/l10n/gl.js b/apps/files/l10n/gl.js index 09641f25ab8..b27f426d550 100644 --- a/apps/files/l10n/gl.js +++ b/apps/files/l10n/gl.js @@ -219,6 +219,7 @@ OC.L10N.register( "Show those shortcuts" : "Amosar eses atallos", "You" : "Vde.", "Shared multiple times with different people" : "Compartido varias veces con diferentes persoas", + "Unable to change the favorite state of the file" : "Non é posíbel cambiar o estado de favorito do ficheiro", "Error while loading the file data" : "Produciuse un erro ao cargar os datos do ficheiro", "Owner" : "Propietario", "Remove from favorites" : "Retirar de favoritos", diff --git a/apps/files/l10n/gl.json b/apps/files/l10n/gl.json index 05b3ac5d05d..ff492c979ff 100644 --- a/apps/files/l10n/gl.json +++ b/apps/files/l10n/gl.json @@ -217,6 +217,7 @@ "Show those shortcuts" : "Amosar eses atallos", "You" : "Vde.", "Shared multiple times with different people" : "Compartido varias veces con diferentes persoas", + "Unable to change the favorite state of the file" : "Non é posíbel cambiar o estado de favorito do ficheiro", "Error while loading the file data" : "Produciuse un erro ao cargar os datos do ficheiro", "Owner" : "Propietario", "Remove from favorites" : "Retirar de favoritos", diff --git a/apps/files/l10n/hu.js b/apps/files/l10n/hu.js index 49059d64de2..24e9c70a523 100644 --- a/apps/files/l10n/hu.js +++ b/apps/files/l10n/hu.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Gyorsbillentyűk megjelenítése", "You" : "Ön", "Shared multiple times with different people" : "Többször megosztva különböző személyekkel", + "Unable to change the favorite state of the file" : "Nem lehet megváltoztatni a fájl kedvenc állapotát", "Error while loading the file data" : "Hiba történt a fájladatok betöltése közben", "Owner" : "Tulajdonos", "Remove from favorites" : "Eltávolítás a kedvencekből", diff --git a/apps/files/l10n/hu.json b/apps/files/l10n/hu.json index 42501ffa19c..e291c309b34 100644 --- a/apps/files/l10n/hu.json +++ b/apps/files/l10n/hu.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Gyorsbillentyűk megjelenítése", "You" : "Ön", "Shared multiple times with different people" : "Többször megosztva különböző személyekkel", + "Unable to change the favorite state of the file" : "Nem lehet megváltoztatni a fájl kedvenc állapotát", "Error while loading the file data" : "Hiba történt a fájladatok betöltése közben", "Owner" : "Tulajdonos", "Remove from favorites" : "Eltávolítás a kedvencekből", diff --git a/apps/files/l10n/is.js b/apps/files/l10n/is.js index 550c0c9c4fc..744a4332569 100644 --- a/apps/files/l10n/is.js +++ b/apps/files/l10n/is.js @@ -206,6 +206,7 @@ OC.L10N.register( "Show those shortcuts" : "Sýna þessa flýtilykla", "You" : "Þú", "Shared multiple times with different people" : "Deilt mörgum sinnum með mismunandi fólki", + "Unable to change the favorite state of the file" : "Get ekki breytt stöðu sem eftirlæti á skránni", "Error while loading the file data" : "Villa við að hlaða inn skráagögnum", "Owner" : "Eigandi", "Remove from favorites" : "Fjarlægja úr eftirlætum", diff --git a/apps/files/l10n/is.json b/apps/files/l10n/is.json index e296f20ebb1..4a2722125c6 100644 --- a/apps/files/l10n/is.json +++ b/apps/files/l10n/is.json @@ -204,6 +204,7 @@ "Show those shortcuts" : "Sýna þessa flýtilykla", "You" : "Þú", "Shared multiple times with different people" : "Deilt mörgum sinnum með mismunandi fólki", + "Unable to change the favorite state of the file" : "Get ekki breytt stöðu sem eftirlæti á skránni", "Error while loading the file data" : "Villa við að hlaða inn skráagögnum", "Owner" : "Eigandi", "Remove from favorites" : "Fjarlægja úr eftirlætum", diff --git a/apps/files/l10n/it.js b/apps/files/l10n/it.js index c0c1be3d760..614a38dfee8 100644 --- a/apps/files/l10n/it.js +++ b/apps/files/l10n/it.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Mostra quelle scorciatoie", "You" : "Tu", "Shared multiple times with different people" : "Condiviso più volte con diverse persone", + "Unable to change the favorite state of the file" : "Impossibile modificare lo stato preferito del file", "Error while loading the file data" : "Errore durante il caricamento del file di dati", "Owner" : "Proprietario", "Remove from favorites" : "Rimuovi dai preferiti", diff --git a/apps/files/l10n/it.json b/apps/files/l10n/it.json index e7450c4fcf7..46c1ad54125 100644 --- a/apps/files/l10n/it.json +++ b/apps/files/l10n/it.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Mostra quelle scorciatoie", "You" : "Tu", "Shared multiple times with different people" : "Condiviso più volte con diverse persone", + "Unable to change the favorite state of the file" : "Impossibile modificare lo stato preferito del file", "Error while loading the file data" : "Errore durante il caricamento del file di dati", "Owner" : "Proprietario", "Remove from favorites" : "Rimuovi dai preferiti", diff --git a/apps/files/l10n/ja.js b/apps/files/l10n/ja.js index c079fb21063..2ba264d20ea 100644 --- a/apps/files/l10n/ja.js +++ b/apps/files/l10n/ja.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "これらのショートカットを表示する", "You" : "自分", "Shared multiple times with different people" : "異なる人と複数回共有", + "Unable to change the favorite state of the file" : "ファイルのお気に入りの状態を変更できません", "Error while loading the file data" : "ファイルデータの読み込み中にエラーが発生しました", "Owner" : "作成者", "Remove from favorites" : "お気に入りから削除", diff --git a/apps/files/l10n/ja.json b/apps/files/l10n/ja.json index 67a1afa9371..ccdec912f64 100644 --- a/apps/files/l10n/ja.json +++ b/apps/files/l10n/ja.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "これらのショートカットを表示する", "You" : "自分", "Shared multiple times with different people" : "異なる人と複数回共有", + "Unable to change the favorite state of the file" : "ファイルのお気に入りの状態を変更できません", "Error while loading the file data" : "ファイルデータの読み込み中にエラーが発生しました", "Owner" : "作成者", "Remove from favorites" : "お気に入りから削除", diff --git a/apps/files/l10n/pl.js b/apps/files/l10n/pl.js index 0fbe930ae76..b10cdb2380f 100644 --- a/apps/files/l10n/pl.js +++ b/apps/files/l10n/pl.js @@ -227,6 +227,7 @@ OC.L10N.register( "Show those shortcuts" : "Pokaż te skróty", "You" : "Ty", "Shared multiple times with different people" : "Udostępniony wiele razy różnym osobom", + "Unable to change the favorite state of the file" : "Nie można zmienić ulubionego stanu pliku", "Error while loading the file data" : "Błąd podczas wczytywania danych pliku", "Owner" : "Właściciel", "Remove from favorites" : "Usuń z ulubionych", diff --git a/apps/files/l10n/pl.json b/apps/files/l10n/pl.json index 25dbcf89c01..f54507e7ff7 100644 --- a/apps/files/l10n/pl.json +++ b/apps/files/l10n/pl.json @@ -225,6 +225,7 @@ "Show those shortcuts" : "Pokaż te skróty", "You" : "Ty", "Shared multiple times with different people" : "Udostępniony wiele razy różnym osobom", + "Unable to change the favorite state of the file" : "Nie można zmienić ulubionego stanu pliku", "Error while loading the file data" : "Błąd podczas wczytywania danych pliku", "Owner" : "Właściciel", "Remove from favorites" : "Usuń z ulubionych", diff --git a/apps/files/l10n/pt_BR.js b/apps/files/l10n/pt_BR.js index f4b6ddd5c53..722c29d09bb 100644 --- a/apps/files/l10n/pt_BR.js +++ b/apps/files/l10n/pt_BR.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Mostrar esses atalhos", "You" : "Você", "Shared multiple times with different people" : "Compartilhado várias vezes com pessoas diferentes", + "Unable to change the favorite state of the file" : "Impossível mudar estado de favorito do arquivo", "Error while loading the file data" : "Erro ao carregar os dados do arquivo", "Owner" : "Proprietário", "Remove from favorites" : "Excluir dos favoritos", diff --git a/apps/files/l10n/pt_BR.json b/apps/files/l10n/pt_BR.json index fc695c4f21b..8f361e72cf8 100644 --- a/apps/files/l10n/pt_BR.json +++ b/apps/files/l10n/pt_BR.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Mostrar esses atalhos", "You" : "Você", "Shared multiple times with different people" : "Compartilhado várias vezes com pessoas diferentes", + "Unable to change the favorite state of the file" : "Impossível mudar estado de favorito do arquivo", "Error while loading the file data" : "Erro ao carregar os dados do arquivo", "Owner" : "Proprietário", "Remove from favorites" : "Excluir dos favoritos", diff --git a/apps/files/l10n/ru.js b/apps/files/l10n/ru.js index 7b4c7c05c47..4f6cf29409b 100644 --- a/apps/files/l10n/ru.js +++ b/apps/files/l10n/ru.js @@ -227,6 +227,7 @@ OC.L10N.register( "Show those shortcuts" : "Показать ярлыки", "You" : "Вы", "Shared multiple times with different people" : "Делиться несколько раз с разными людьми", + "Unable to change the favorite state of the file" : "Невозможно изменить избранное состояние файла", "Error while loading the file data" : "Не удалось загрузить данные файла", "Owner" : "Владелец", "Remove from favorites" : "Удалить из избранных", diff --git a/apps/files/l10n/ru.json b/apps/files/l10n/ru.json index 9b5bb313718..32c53285922 100644 --- a/apps/files/l10n/ru.json +++ b/apps/files/l10n/ru.json @@ -225,6 +225,7 @@ "Show those shortcuts" : "Показать ярлыки", "You" : "Вы", "Shared multiple times with different people" : "Делиться несколько раз с разными людьми", + "Unable to change the favorite state of the file" : "Невозможно изменить избранное состояние файла", "Error while loading the file data" : "Не удалось загрузить данные файла", "Owner" : "Владелец", "Remove from favorites" : "Удалить из избранных", diff --git a/apps/files/l10n/sk.js b/apps/files/l10n/sk.js index abdd49b9678..bcb44beb3f3 100644 --- a/apps/files/l10n/sk.js +++ b/apps/files/l10n/sk.js @@ -219,6 +219,7 @@ OC.L10N.register( "Show those shortcuts" : "Zobraziť klávesové skratky", "You" : "Vy", "Shared multiple times with different people" : "Zdieľané viackrát rôznymi ľuďmi", + "Unable to change the favorite state of the file" : "Nie je možné zmeniť stav „obľúbené“ súboru", "Error while loading the file data" : "Chyba pri načítaní údajov súboru", "Owner" : "Vlastník", "Remove from favorites" : "Odstrániť z obľúbených", diff --git a/apps/files/l10n/sk.json b/apps/files/l10n/sk.json index 97ab5fb9073..d9d2a6212d4 100644 --- a/apps/files/l10n/sk.json +++ b/apps/files/l10n/sk.json @@ -217,6 +217,7 @@ "Show those shortcuts" : "Zobraziť klávesové skratky", "You" : "Vy", "Shared multiple times with different people" : "Zdieľané viackrát rôznymi ľuďmi", + "Unable to change the favorite state of the file" : "Nie je možné zmeniť stav „obľúbené“ súboru", "Error while loading the file data" : "Chyba pri načítaní údajov súboru", "Owner" : "Vlastník", "Remove from favorites" : "Odstrániť z obľúbených", diff --git a/apps/files/l10n/sr.js b/apps/files/l10n/sr.js index 7437c33f38c..7c26a7c95bd 100644 --- a/apps/files/l10n/sr.js +++ b/apps/files/l10n/sr.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Прикажи те пречице", "You" : "Ви", "Shared multiple times with different people" : "Дељено више пута са разним људима", + "Unable to change the favorite state of the file" : "Није успела измена стања омиљено за фајл", "Error while loading the file data" : "Грешка при учитавању података фајла", "Owner" : "Власник", "Remove from favorites" : "Избаци из омиљених", diff --git a/apps/files/l10n/sr.json b/apps/files/l10n/sr.json index 1ae51f59dba..c262023b8f3 100644 --- a/apps/files/l10n/sr.json +++ b/apps/files/l10n/sr.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Прикажи те пречице", "You" : "Ви", "Shared multiple times with different people" : "Дељено више пута са разним људима", + "Unable to change the favorite state of the file" : "Није успела измена стања омиљено за фајл", "Error while loading the file data" : "Грешка при учитавању података фајла", "Owner" : "Власник", "Remove from favorites" : "Избаци из омиљених", diff --git a/apps/files/l10n/sv.js b/apps/files/l10n/sv.js index 229ae071fc7..27d6814448f 100644 --- a/apps/files/l10n/sv.js +++ b/apps/files/l10n/sv.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Visa dessa genvägar", "You" : "Du", "Shared multiple times with different people" : "Delad flera gånger med olika personer", + "Unable to change the favorite state of the file" : "Kan inte ändra filens favoritstatus", "Error while loading the file data" : "Fel vid inläsning av fildata", "Owner" : "Ägare", "Remove from favorites" : "Ta bort från favoriter", diff --git a/apps/files/l10n/sv.json b/apps/files/l10n/sv.json index 35788f19371..3944641dad5 100644 --- a/apps/files/l10n/sv.json +++ b/apps/files/l10n/sv.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Visa dessa genvägar", "You" : "Du", "Shared multiple times with different people" : "Delad flera gånger med olika personer", + "Unable to change the favorite state of the file" : "Kan inte ändra filens favoritstatus", "Error while loading the file data" : "Fel vid inläsning av fildata", "Owner" : "Ägare", "Remove from favorites" : "Ta bort från favoriter", diff --git a/apps/files/l10n/tr.js b/apps/files/l10n/tr.js index 968c8d71d14..829f9438f3b 100644 --- a/apps/files/l10n/tr.js +++ b/apps/files/l10n/tr.js @@ -70,7 +70,9 @@ OC.L10N.register( "Transferred from %1$s on %2$s" : "%1$s üzerinden %2$s zamanında aktarıldı", "Files compatibility" : "Dosyalar uygulaması uyumluluğu", "Allow to restrict filenames to ensure files can be synced with all clients. By default all filenames valid on POSIX (e.g. Linux or macOS) are allowed." : "Dosyaların tüm istemcilerle eşitlenebilmesi için dosya adlarının kısıtlanmasını sağlar. Varsayılan olarak POSIX içinde geçerli olan tüm dosya adlarına (Linux veya macOS) izin verilir.", - "Enforce Windows compatibility" : "Windows uyumluluğu dayatılsın", + "After enabling the Windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner." : "Windows uyumlu dosya adları açıldıktan sonra, var olan dosyalar artık değiştirilemez. Ancak sahipleri tarafından geçerli yeni adlarla yeniden adlandırılabilir.", + "It is also possible to migrate files automatically after enabling this setting, please refer to the documentation about the occ command." : "Bu ayarı açtıktan sonra dosyaları otomatik olarak da aktarılabilir. Lütfen occ komutuyla ilgili belgelere bakın.", + "Enforce Windows compatibility" : "Windows uyumluluğu zorunlu kılınsın", "This will block filenames not valid on Windows systems, like using reserved names or special characters. But this will not enforce compatibility of case sensitivity." : "Ayrılmış adlar veya özel karakterler kullanmak gibi Windows sistemlerinde geçerli olmayan dosya adlarını engeller. Ancak büyük/küçük harf duyarlılığı uyumluluğunu zorunlu kılmaz.", "File Management" : "Dosya yönetimi", "Home" : "Giriş", @@ -95,6 +97,11 @@ OC.L10N.register( "Renamed \"{oldName}\" to \"{newName}\"" : "\"{oldName}\" adı \"{newName}\" olarak değiştirildi", "Rename file" : "Dosyayı yeniden adlandır", "Folder" : "Klasör", + "Unknown file type" : "Dosya türü bilinmiyor", + "{ext} image" : "{ext} görsel", + "{ext} video" : "{ext} görüntü", + "{ext} audio" : "{ext} ses", + "{ext} text" : "{ext} metin", "Pending" : "Bekliyor", "Unknown date" : "Tarih bilinmiyor", "Clear filter" : "Süzgeci temizle", @@ -105,6 +112,7 @@ OC.L10N.register( "Total rows summary" : "Toplam satır özeti", "Toggle selection for all files and folders" : "Tüm dosyaları ve klasörleri seç/bırak", "Name" : "Ad", + "File type" : "Dosya türü", "Size" : "Boyut", "\"{displayName}\" failed on some elements" : "\"{displayName}\" bazı bileşenlerde tamamlanamadı", "\"{displayName}\" batch action executed successfully" : "\"{displayName}\" toplu işlemi tamamlandı", @@ -183,6 +191,7 @@ OC.L10N.register( "Sort favorites first" : "Sık kullanılanlar üstte sıralansın", "Sort folders before files" : "Klasörler dosyaların üzerinde sıralansın", "Show hidden files" : "Gizli dosyaları görüntüle", + "Show file type column" : "Dosya türü sütunu görüntülensin", "Crop image previews" : "Görsel ön izlemeleri kırpılsın", "Enable the grid view" : "Tablo görünümü kullanılsın", "Enable folder tree" : "Klasör ağacını aç", @@ -219,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Bu kısayolları görüntüler", "You" : "Siz", "Shared multiple times with different people" : "Farklı kişilerle birkaç kez paylaşılmış", + "Unable to change the favorite state of the file" : "Dosyanın sık kullanılan durumu değiştirilemedi", "Error while loading the file data" : "Dosya verileri yüklenirken sorun çıktı", "Owner" : "Sahibi", "Remove from favorites" : "Sık kullanılanlardan kaldır", @@ -334,6 +344,7 @@ OC.L10N.register( "Unexpected error: {error}" : "Beklenmeyen bir sorun çıktı: {error}", "_%n file_::_%n files_" : ["%n dosya","%n dosya"], "_%n folder_::_%n folders_" : ["%n klasör","%n klasör"], + "_%n hidden_::_%n hidden_" : ["%n gizli","%n gizli"], "Filename must not be empty." : "Dosya adı boş olamaz.", "\"{char}\" is not allowed inside a filename." : "Dosya adlarında \"{char}\" kullanılamaz.", "\"{segment}\" is a reserved name and not allowed for filenames." : "\"{segment}\" ayrılmış bir ad olduğundan dosya adlarında kullanılamaz.", @@ -444,6 +455,9 @@ OC.L10N.register( "{fileCount} files and {folderCount} folders" : "{fileCount} dosya ve {folderCount} klasör", "Personal Files" : "Kişisel dosyalar", "Text file" : "Metin dosyası", - "New text file.txt" : "Yeni metin dosyası.txt" + "New text file.txt" : "Yeni metin dosyası.txt", + "%1$s (renamed)" : "%1$s (yeniden adlandırıldı)", + "renamed file" : "dosyayı yeniden adlandırdı", + "After enabling the windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner." : "Windows uyumlu dosya adları açıldıktan sonra, var olan dosyalar artık değiştirilemez. Ancak sahipleri tarafından geçerli yeni adlarla yeniden adlandırılabilir." }, "nplurals=2; plural=(n > 1);"); diff --git a/apps/files/l10n/tr.json b/apps/files/l10n/tr.json index ae5760a8a05..a83f1db0285 100644 --- a/apps/files/l10n/tr.json +++ b/apps/files/l10n/tr.json @@ -68,7 +68,9 @@ "Transferred from %1$s on %2$s" : "%1$s üzerinden %2$s zamanında aktarıldı", "Files compatibility" : "Dosyalar uygulaması uyumluluğu", "Allow to restrict filenames to ensure files can be synced with all clients. By default all filenames valid on POSIX (e.g. Linux or macOS) are allowed." : "Dosyaların tüm istemcilerle eşitlenebilmesi için dosya adlarının kısıtlanmasını sağlar. Varsayılan olarak POSIX içinde geçerli olan tüm dosya adlarına (Linux veya macOS) izin verilir.", - "Enforce Windows compatibility" : "Windows uyumluluğu dayatılsın", + "After enabling the Windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner." : "Windows uyumlu dosya adları açıldıktan sonra, var olan dosyalar artık değiştirilemez. Ancak sahipleri tarafından geçerli yeni adlarla yeniden adlandırılabilir.", + "It is also possible to migrate files automatically after enabling this setting, please refer to the documentation about the occ command." : "Bu ayarı açtıktan sonra dosyaları otomatik olarak da aktarılabilir. Lütfen occ komutuyla ilgili belgelere bakın.", + "Enforce Windows compatibility" : "Windows uyumluluğu zorunlu kılınsın", "This will block filenames not valid on Windows systems, like using reserved names or special characters. But this will not enforce compatibility of case sensitivity." : "Ayrılmış adlar veya özel karakterler kullanmak gibi Windows sistemlerinde geçerli olmayan dosya adlarını engeller. Ancak büyük/küçük harf duyarlılığı uyumluluğunu zorunlu kılmaz.", "File Management" : "Dosya yönetimi", "Home" : "Giriş", @@ -93,6 +95,11 @@ "Renamed \"{oldName}\" to \"{newName}\"" : "\"{oldName}\" adı \"{newName}\" olarak değiştirildi", "Rename file" : "Dosyayı yeniden adlandır", "Folder" : "Klasör", + "Unknown file type" : "Dosya türü bilinmiyor", + "{ext} image" : "{ext} görsel", + "{ext} video" : "{ext} görüntü", + "{ext} audio" : "{ext} ses", + "{ext} text" : "{ext} metin", "Pending" : "Bekliyor", "Unknown date" : "Tarih bilinmiyor", "Clear filter" : "Süzgeci temizle", @@ -103,6 +110,7 @@ "Total rows summary" : "Toplam satır özeti", "Toggle selection for all files and folders" : "Tüm dosyaları ve klasörleri seç/bırak", "Name" : "Ad", + "File type" : "Dosya türü", "Size" : "Boyut", "\"{displayName}\" failed on some elements" : "\"{displayName}\" bazı bileşenlerde tamamlanamadı", "\"{displayName}\" batch action executed successfully" : "\"{displayName}\" toplu işlemi tamamlandı", @@ -181,6 +189,7 @@ "Sort favorites first" : "Sık kullanılanlar üstte sıralansın", "Sort folders before files" : "Klasörler dosyaların üzerinde sıralansın", "Show hidden files" : "Gizli dosyaları görüntüle", + "Show file type column" : "Dosya türü sütunu görüntülensin", "Crop image previews" : "Görsel ön izlemeleri kırpılsın", "Enable the grid view" : "Tablo görünümü kullanılsın", "Enable folder tree" : "Klasör ağacını aç", @@ -217,6 +226,7 @@ "Show those shortcuts" : "Bu kısayolları görüntüler", "You" : "Siz", "Shared multiple times with different people" : "Farklı kişilerle birkaç kez paylaşılmış", + "Unable to change the favorite state of the file" : "Dosyanın sık kullanılan durumu değiştirilemedi", "Error while loading the file data" : "Dosya verileri yüklenirken sorun çıktı", "Owner" : "Sahibi", "Remove from favorites" : "Sık kullanılanlardan kaldır", @@ -332,6 +342,7 @@ "Unexpected error: {error}" : "Beklenmeyen bir sorun çıktı: {error}", "_%n file_::_%n files_" : ["%n dosya","%n dosya"], "_%n folder_::_%n folders_" : ["%n klasör","%n klasör"], + "_%n hidden_::_%n hidden_" : ["%n gizli","%n gizli"], "Filename must not be empty." : "Dosya adı boş olamaz.", "\"{char}\" is not allowed inside a filename." : "Dosya adlarında \"{char}\" kullanılamaz.", "\"{segment}\" is a reserved name and not allowed for filenames." : "\"{segment}\" ayrılmış bir ad olduğundan dosya adlarında kullanılamaz.", @@ -442,6 +453,9 @@ "{fileCount} files and {folderCount} folders" : "{fileCount} dosya ve {folderCount} klasör", "Personal Files" : "Kişisel dosyalar", "Text file" : "Metin dosyası", - "New text file.txt" : "Yeni metin dosyası.txt" + "New text file.txt" : "Yeni metin dosyası.txt", + "%1$s (renamed)" : "%1$s (yeniden adlandırıldı)", + "renamed file" : "dosyayı yeniden adlandırdı", + "After enabling the windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner." : "Windows uyumlu dosya adları açıldıktan sonra, var olan dosyalar artık değiştirilemez. Ancak sahipleri tarafından geçerli yeni adlarla yeniden adlandırılabilir." },"pluralForm" :"nplurals=2; plural=(n > 1);" }
\ No newline at end of file diff --git a/apps/files/l10n/uk.js b/apps/files/l10n/uk.js index c82eabc2132..e1059df3ed0 100644 --- a/apps/files/l10n/uk.js +++ b/apps/files/l10n/uk.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "Показувати ці скорочення", "You" : "Ви", "Shared multiple times with different people" : "Поділилися кілька разів з різними людьми", + "Unable to change the favorite state of the file" : "Неможливо змінити статус вподобаного файлу", "Error while loading the file data" : "Помилка під час отримання даних щодо файлу", "Owner" : "Власник", "Remove from favorites" : "Прибрати зірочку", diff --git a/apps/files/l10n/uk.json b/apps/files/l10n/uk.json index 722583480c3..a0ce459f192 100644 --- a/apps/files/l10n/uk.json +++ b/apps/files/l10n/uk.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "Показувати ці скорочення", "You" : "Ви", "Shared multiple times with different people" : "Поділилися кілька разів з різними людьми", + "Unable to change the favorite state of the file" : "Неможливо змінити статус вподобаного файлу", "Error while loading the file data" : "Помилка під час отримання даних щодо файлу", "Owner" : "Власник", "Remove from favorites" : "Прибрати зірочку", diff --git a/apps/files/l10n/zh_CN.js b/apps/files/l10n/zh_CN.js index fc8bfeb65a9..1309bf4379f 100644 --- a/apps/files/l10n/zh_CN.js +++ b/apps/files/l10n/zh_CN.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "显示快捷键", "You" : "您", "Shared multiple times with different people" : "与不同的用户多次分享", + "Unable to change the favorite state of the file" : "无法更改文件的收藏状态", "Error while loading the file data" : "加载文件数据时出错", "Owner" : "拥有者", "Remove from favorites" : "从收藏中移除", diff --git a/apps/files/l10n/zh_CN.json b/apps/files/l10n/zh_CN.json index 2adb4da98d8..aca369c6619 100644 --- a/apps/files/l10n/zh_CN.json +++ b/apps/files/l10n/zh_CN.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "显示快捷键", "You" : "您", "Shared multiple times with different people" : "与不同的用户多次分享", + "Unable to change the favorite state of the file" : "无法更改文件的收藏状态", "Error while loading the file data" : "加载文件数据时出错", "Owner" : "拥有者", "Remove from favorites" : "从收藏中移除", diff --git a/apps/files/l10n/zh_HK.js b/apps/files/l10n/zh_HK.js index 0fc87c590dc..12e62becfe7 100644 --- a/apps/files/l10n/zh_HK.js +++ b/apps/files/l10n/zh_HK.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "顯示這些快捷鍵", "You" : "您", "Shared multiple times with different people" : "與不同的人多次分享", + "Unable to change the favorite state of the file" : "無法更改檔案的最愛狀態", "Error while loading the file data" : "載入檔案資料失敗", "Owner" : "擁有者", "Remove from favorites" : "從最愛中移除", diff --git a/apps/files/l10n/zh_HK.json b/apps/files/l10n/zh_HK.json index 8c4e9b0b4cd..c211e9cf075 100644 --- a/apps/files/l10n/zh_HK.json +++ b/apps/files/l10n/zh_HK.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "顯示這些快捷鍵", "You" : "您", "Shared multiple times with different people" : "與不同的人多次分享", + "Unable to change the favorite state of the file" : "無法更改檔案的最愛狀態", "Error while loading the file data" : "載入檔案資料失敗", "Owner" : "擁有者", "Remove from favorites" : "從最愛中移除", diff --git a/apps/files/l10n/zh_TW.js b/apps/files/l10n/zh_TW.js index 46450967a8d..cfdfa03dc07 100644 --- a/apps/files/l10n/zh_TW.js +++ b/apps/files/l10n/zh_TW.js @@ -228,6 +228,7 @@ OC.L10N.register( "Show those shortcuts" : "顯示這些快捷鍵", "You" : "您", "Shared multiple times with different people" : "與不同的人多次分享", + "Unable to change the favorite state of the file" : "無法變更檔案的喜愛狀態", "Error while loading the file data" : "載入檔案資料時發生錯誤", "Owner" : "擁有者", "Remove from favorites" : "從喜愛中移除", diff --git a/apps/files/l10n/zh_TW.json b/apps/files/l10n/zh_TW.json index 4ba080ebd64..4af203b309e 100644 --- a/apps/files/l10n/zh_TW.json +++ b/apps/files/l10n/zh_TW.json @@ -226,6 +226,7 @@ "Show those shortcuts" : "顯示這些快捷鍵", "You" : "您", "Shared multiple times with different people" : "與不同的人多次分享", + "Unable to change the favorite state of the file" : "無法變更檔案的喜愛狀態", "Error while loading the file data" : "載入檔案資料時發生錯誤", "Owner" : "擁有者", "Remove from favorites" : "從喜愛中移除", diff --git a/apps/files/lib/Activity/FavoriteProvider.php b/apps/files/lib/Activity/FavoriteProvider.php index 7a660287192..e56b13b902a 100644 --- a/apps/files/lib/Activity/FavoriteProvider.php +++ b/apps/files/lib/Activity/FavoriteProvider.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Filter/Favorites.php b/apps/files/lib/Activity/Filter/Favorites.php index 6683f0bc44e..0159dd20b82 100644 --- a/apps/files/lib/Activity/Filter/Favorites.php +++ b/apps/files/lib/Activity/Filter/Favorites.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Filter/FileChanges.php b/apps/files/lib/Activity/Filter/FileChanges.php index 215510147d6..0ca8f6792e0 100644 --- a/apps/files/lib/Activity/Filter/FileChanges.php +++ b/apps/files/lib/Activity/Filter/FileChanges.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Provider.php b/apps/files/lib/Activity/Provider.php index faa2bbd0b3b..3ef79ac107f 100644 --- a/apps/files/lib/Activity/Provider.php +++ b/apps/files/lib/Activity/Provider.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Settings/FavoriteAction.php b/apps/files/lib/Activity/Settings/FavoriteAction.php index b572a9546e0..73b200341ec 100644 --- a/apps/files/lib/Activity/Settings/FavoriteAction.php +++ b/apps/files/lib/Activity/Settings/FavoriteAction.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Settings/FileChanged.php b/apps/files/lib/Activity/Settings/FileChanged.php index 5af87456550..c33ed5e1eba 100644 --- a/apps/files/lib/Activity/Settings/FileChanged.php +++ b/apps/files/lib/Activity/Settings/FileChanged.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Settings/FileFavoriteChanged.php b/apps/files/lib/Activity/Settings/FileFavoriteChanged.php index e4ac572f3f3..5000902ed3f 100644 --- a/apps/files/lib/Activity/Settings/FileFavoriteChanged.php +++ b/apps/files/lib/Activity/Settings/FileFavoriteChanged.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Command/DeleteOrphanedFiles.php b/apps/files/lib/Command/DeleteOrphanedFiles.php index 0f3375d2e93..37cb3159f4a 100644 --- a/apps/files/lib/Command/DeleteOrphanedFiles.php +++ b/apps/files/lib/Command/DeleteOrphanedFiles.php @@ -54,7 +54,7 @@ class DeleteOrphanedFiles extends Command { $deletedMounts = $this->cleanupOrphanedMounts(); $output->writeln("$deletedMounts orphaned mount entries deleted"); - + return self::SUCCESS; } @@ -112,7 +112,7 @@ class DeleteOrphanedFiles extends Command { return $deletedEntries; } - + /** * @param array<int, int[]> $fileIdsByStorage * @return int diff --git a/apps/files/lib/Command/SanitizeFilenames.php b/apps/files/lib/Command/SanitizeFilenames.php index a06b820ee6c..88d41d1cb5e 100644 --- a/apps/files/lib/Command/SanitizeFilenames.php +++ b/apps/files/lib/Command/SanitizeFilenames.php @@ -62,7 +62,7 @@ class SanitizeFilenames extends Base { mode: InputOption::VALUE_REQUIRED, description: 'Replacement for invalid character (by default space, underscore or dash is used)', ); - + } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/apps/files/lib/Command/Scan.php b/apps/files/lib/Command/Scan.php index bd99dd28e60..b9057139b0e 100644 --- a/apps/files/lib/Command/Scan.php +++ b/apps/files/lib/Command/Scan.php @@ -286,8 +286,8 @@ class Scan extends Base { $this->execTime = -microtime(true); // Convert PHP errors to exceptions set_error_handler( - fn (int $severity, string $message, string $file, int $line): bool => - $this->exceptionErrorHandler($output, $severity, $message, $file, $line), + fn (int $severity, string $message, string $file, int $line): bool + => $this->exceptionErrorHandler($output, $severity, $message, $file, $line), E_ALL ); } diff --git a/apps/files/lib/Command/ScanAppData.php b/apps/files/lib/Command/ScanAppData.php index 81c80cab373..0e08c6a8cfe 100644 --- a/apps/files/lib/Command/ScanAppData.php +++ b/apps/files/lib/Command/ScanAppData.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Command/TransferOwnership.php b/apps/files/lib/Command/TransferOwnership.php index 104a8fb4985..f7663e26f28 100644 --- a/apps/files/lib/Command/TransferOwnership.php +++ b/apps/files/lib/Command/TransferOwnership.php @@ -64,7 +64,7 @@ class TransferOwnership extends Command { 'transfer-incoming-shares', null, InputOption::VALUE_OPTIONAL, - 'transfer incoming user file shares to destination user. Usage: --transfer-incoming-shares=1 (value required)', + 'Incoming shares are always transferred now, so this option does not affect the ownership transfer anymore', '2' )->addOption( 'include-external-storage', @@ -129,27 +129,6 @@ class TransferOwnership extends Command { } try { - $includeIncomingArgument = $input->getOption('transfer-incoming-shares'); - - switch ($includeIncomingArgument) { - case '0': - $includeIncoming = false; - break; - case '1': - $includeIncoming = true; - break; - case '2': - $includeIncoming = $this->config->getSystemValue('transferIncomingShares', false); - if (gettype($includeIncoming) !== 'boolean') { - $output->writeln("<error> config.php: 'transfer-incoming-shares': wrong usage. Transfer aborted.</error>"); - return self::FAILURE; - } - break; - default: - $output->writeln('<error>Option --transfer-incoming-shares: wrong usage. Transfer aborted.</error>'); - return self::FAILURE; - } - $this->transferService->transfer( $sourceUserObject, $destinationUserObject, @@ -157,7 +136,6 @@ class TransferOwnership extends Command { $output, $input->getOption('move') === true, false, - $includeIncoming, $includeExternalStorage, ); } catch (TransferOwnershipException $e) { diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index 9c683b7f41f..8bb024fb698 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -105,11 +105,12 @@ class ApiController extends Controller { } // Validate the user is allowed to download the file (preview is some kind of download) + /** @var ISharedStorage $storage */ $storage = $file->getStorage(); if ($storage->instanceOfStorage(ISharedStorage::class)) { - /** @var ISharedStorage $storage */ - $attributes = $storage->getShare()->getAttributes(); - if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) { + /** @var IShare $share */ + $share = $storage->getShare(); + if (!$share->canSeeContent()) { throw new NotFoundException(); } } diff --git a/apps/files/lib/Controller/DirectEditingController.php b/apps/files/lib/Controller/DirectEditingController.php index 2c910006df5..c8addc33e98 100644 --- a/apps/files/lib/Controller/DirectEditingController.php +++ b/apps/files/lib/Controller/DirectEditingController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Controller/DirectEditingViewController.php b/apps/files/lib/Controller/DirectEditingViewController.php index 1d78e2af0e0..b13e68f7766 100644 --- a/apps/files/lib/Controller/DirectEditingViewController.php +++ b/apps/files/lib/Controller/DirectEditingViewController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Service/DirectEditingService.php b/apps/files/lib/Service/DirectEditingService.php index 243ddebdc67..3d756ee56fa 100644 --- a/apps/files/lib/Service/DirectEditingService.php +++ b/apps/files/lib/Service/DirectEditingService.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php index b3a36ee13e5..84c99f32109 100644 --- a/apps/files/lib/Service/OwnershipTransferService.php +++ b/apps/files/lib/Service/OwnershipTransferService.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace OCA\Files\Service; use Closure; +use Exception; use OC\Files\Filesystem; use OC\Files\View; use OC\User\NoUserException; @@ -19,6 +20,7 @@ use OCA\Files_External\Config\ConfigAdapter; use OCP\Encryption\IManager as IEncryptionManager; use OCP\Files\Config\IHomeMountProvider; use OCP\Files\Config\IUserMountCache; +use OCP\Files\File; use OCP\Files\FileInfo; use OCP\Files\InvalidPathException; use OCP\Files\IRootFolder; @@ -70,7 +72,6 @@ class OwnershipTransferService { ?OutputInterface $output = null, bool $move = false, bool $firstLogin = false, - bool $transferIncomingShares = false, bool $includeExternalStorage = false, ): void { $output = $output ?? new NullOutput(); @@ -157,29 +158,26 @@ class OwnershipTransferService { $sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize(); // transfer the incoming shares - if ($transferIncomingShares === true) { - $sourceShares = $this->collectIncomingShares( - $sourceUid, - $output, - $view - ); - $destinationShares = $this->collectIncomingShares( - $destinationUid, - $output, - $view, - true - ); - $this->transferIncomingShares( - $sourceUid, - $destinationUid, - $sourceShares, - $destinationShares, - $output, - $path, - $finalTarget, - $move - ); - } + $sourceShares = $this->collectIncomingShares( + $sourceUid, + $output, + $sourcePath, + ); + $destinationShares = $this->collectIncomingShares( + $destinationUid, + $output, + null, + ); + $this->transferIncomingShares( + $sourceUid, + $destinationUid, + $sourceShares, + $destinationShares, + $output, + $path, + $finalTarget, + $move + ); $destinationPath = $finalTarget . '/' . $path; // restore the shares @@ -258,8 +256,8 @@ class OwnershipTransferService { $mount = $fileInfo->getMountPoint(); // only analyze into folders from main storage, if ( - $mount->getMountProvider() instanceof IHomeMountProvider || - ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter) + $mount->getMountProvider() instanceof IHomeMountProvider + || ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter) ) { if ($fileInfo->isEncrypted()) { /* Encrypted folder means e2ee encrypted, we cannot transfer it */ @@ -344,7 +342,7 @@ class OwnershipTransferService { return mb_strpos( Filesystem::normalizePath($relativePath . '/', false), $normalizedPath . '/') === 0; - } catch (\Exception $e) { + } catch (Exception $e) { return false; } }); @@ -372,14 +370,16 @@ class OwnershipTransferService { }, $shares))); } - private function collectIncomingShares(string $sourceUid, + private function collectIncomingShares( + string $sourceUid, OutputInterface $output, - View $view, - bool $addKeys = false): array { + ?string $path, + ): array { $output->writeln("Collecting all incoming share information for files and folders of $sourceUid ..."); $shares = []; $progress = new ProgressBar($output); + $normalizedPath = Filesystem::normalizePath($path); $offset = 0; while (true) { @@ -388,14 +388,19 @@ class OwnershipTransferService { if (empty($sharePage)) { break; } - if ($addKeys) { - foreach ($sharePage as $singleShare) { - $shares[$singleShare->getNodeId()] = $singleShare; - } - } else { - foreach ($sharePage as $singleShare) { - $shares[] = $singleShare; - } + + if ($path !== null && $path !== "$sourceUid/files") { + $sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) { + try { + return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/'); + } catch (Exception) { + return false; + } + }); + } + + foreach ($sharePage as $share) { + $shares[$share->getNodeId()] = $share; } $offset += 50; @@ -487,8 +492,8 @@ class OwnershipTransferService { foreach ($shares as ['share' => $share, 'suffix' => $suffix]) { try { $output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE); - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedWith() === $destinationUid) { + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedWith() === $destinationUid) { // Unmount the shares before deleting, so we don't try to get the storage later on. $shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget()); if ($shareMountPoint) { @@ -503,8 +508,8 @@ class OwnershipTransferService { $share->setSharedBy($destinationUid); } - if ($share->getShareType() === IShare::TYPE_USER && - !$this->userManager->userExists($share->getSharedWith())) { + if ($share->getShareType() === IShare::TYPE_USER + && !$this->userManager->userExists($share->getSharedWith())) { // stray share with deleted user $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted user "' . $share->getSharedWith() . '", deleting</error>'); $this->shareManager->deleteShare($share); @@ -569,8 +574,8 @@ class OwnershipTransferService { } $shareTarget = $share->getTarget(); $shareTarget = $finalShareTarget . $shareTarget; - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedBy() === $destinationUid) { + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedBy() === $destinationUid) { $this->shareManager->deleteShare($share); } elseif (isset($destinationShares[$share->getNodeId()])) { $destinationShare = $destinationShares[$share->getNodeId()]; diff --git a/apps/files/lib/Service/UserConfig.php b/apps/files/lib/Service/UserConfig.php index a2f2f1c1d14..dee89b990c6 100644 --- a/apps/files/lib/Service/UserConfig.php +++ b/apps/files/lib/Service/UserConfig.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Service/ViewConfig.php b/apps/files/lib/Service/ViewConfig.php index 348ce6596e5..cf8bebd5372 100644 --- a/apps/files/lib/Service/ViewConfig.php +++ b/apps/files/lib/Service/ViewConfig.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -103,7 +104,7 @@ class ViewConfig { if (!in_array($key, $this->getAllowedConfigKeys())) { throw new \InvalidArgumentException('Unknown config key'); } - + if (!in_array($value, $this->getAllowedConfigValues($key)) && !empty($this->getAllowedConfigValues($key))) { throw new \InvalidArgumentException('Invalid config value'); @@ -132,7 +133,7 @@ class ViewConfig { $userId = $this->user->getUID(); $configs = json_decode($this->config->getUserValue($userId, Application::APP_ID, self::CONFIG_KEY, '[]'), true); - + if (!isset($configs[$view])) { $configs[$view] = []; } @@ -158,7 +159,7 @@ class ViewConfig { $userId = $this->user->getUID(); $configs = json_decode($this->config->getUserValue($userId, Application::APP_ID, self::CONFIG_KEY, '[]'), true); $views = array_keys($configs); - + return array_reduce($views, function ($carry, $view) use ($configs) { $carry[$view] = $this->getConfig($view); return $carry; diff --git a/apps/files/src/actions/openInFilesAction.spec.ts b/apps/files/src/actions/openInFilesAction.spec.ts index e732270d4c0..3ccd15fa2d2 100644 --- a/apps/files/src/actions/openInFilesAction.spec.ts +++ b/apps/files/src/actions/openInFilesAction.spec.ts @@ -19,7 +19,7 @@ const recentView = { describe('Open in files action conditions tests', () => { test('Default values', () => { expect(action).toBeInstanceOf(FileAction) - expect(action.id).toBe('open-in-files-recent') + expect(action.id).toBe('open-in-files') expect(action.displayName([], recentView)).toBe('Open in Files') expect(action.iconSvgInline([], recentView)).toBe('') expect(action.default).toBe(DefaultType.HIDDEN) diff --git a/apps/files/src/actions/openInFilesAction.ts b/apps/files/src/actions/openInFilesAction.ts index 10e19e7eace..9e10b1ac74e 100644 --- a/apps/files/src/actions/openInFilesAction.ts +++ b/apps/files/src/actions/openInFilesAction.ts @@ -2,19 +2,21 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { translate as t } from '@nextcloud/l10n' -import { type Node, FileType, FileAction, DefaultType } from '@nextcloud/files' -/** - * TODO: Move away from a redirect and handle - * navigation straight out of the recent view - */ +import type { Node } from '@nextcloud/files' + +import { t } from '@nextcloud/l10n' +import { FileType, FileAction, DefaultType } from '@nextcloud/files' +import { VIEW_ID as SEARCH_VIEW_ID } from '../views/search' + export const action = new FileAction({ - id: 'open-in-files-recent', + id: 'open-in-files', displayName: () => t('files', 'Open in Files'), iconSvgInline: () => '', - enabled: (nodes, view) => view.id === 'recent', + enabled(nodes, view) { + return view.id === 'recent' || view.id === SEARCH_VIEW_ID + }, async exec(node: Node) { let dir = node.dirname diff --git a/apps/files/src/components/FilesNavigationItem.vue b/apps/files/src/components/FilesNavigationItem.vue index 372a83e1441..2c7c8b4b944 100644 --- a/apps/files/src/components/FilesNavigationItem.vue +++ b/apps/files/src/components/FilesNavigationItem.vue @@ -89,7 +89,7 @@ export default defineComponent({ return (Object.values(this.views).reduce((acc, views) => [...acc, ...views], []) as View[]) .filter(view => view.params?.dir.startsWith(this.parent.params?.dir)) } - return this.views[this.parent.id] ?? [] // Root level views have `undefined` parent ids + return this.filterVisible(this.views[this.parent.id] ?? []) }, style() { @@ -103,11 +103,15 @@ export default defineComponent({ }, methods: { + filterVisible(views: View[]) { + return views.filter(({ _view, id }) => id === this.currentView?.id || _view.hidden !== true) + }, + hasChildViews(view: View): boolean { if (this.level >= maxLevel) { return false } - return this.views[view.id]?.length > 0 + return this.filterVisible(this.views[view.id] ?? []).length > 0 }, /** diff --git a/apps/files/src/components/FilesNavigationSearch.vue b/apps/files/src/components/FilesNavigationSearch.vue new file mode 100644 index 00000000000..85dc5534e5e --- /dev/null +++ b/apps/files/src/components/FilesNavigationSearch.vue @@ -0,0 +1,122 @@ +<!-- + - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<script setup lang="ts"> +import { mdiMagnify, mdiSearchWeb } from '@mdi/js' +import { t } from '@nextcloud/l10n' +import { computed } from 'vue' +import NcActions from '@nextcloud/vue/components/NcActions' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSearch' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import { onBeforeNavigation } from '../composables/useBeforeNavigation.ts' +import { useNavigation } from '../composables/useNavigation.ts' +import { useRouteParameters } from '../composables/useRouteParameters.ts' +import { useFilesStore } from '../store/files.ts' +import { useSearchStore } from '../store/search.ts' +import { VIEW_ID } from '../views/search.ts' + +const { currentView } = useNavigation(true) +const { directory } = useRouteParameters() + +const filesStore = useFilesStore() +const searchStore = useSearchStore() + +/** + * When the route is changed from search view to something different + * we need to clear the search box. + */ +onBeforeNavigation((to, from, next) => { + if (to.params.view !== VIEW_ID && from.params.view === VIEW_ID) { + // we are leaving the search view so unset the query + searchStore.query = '' + searchStore.scope = 'filter' + } else if (to.params.view === VIEW_ID && from.params.view === VIEW_ID) { + // fix the query if the user refreshed the view + if (searchStore.query && !to.query.query) { + // @ts-expect-error This is a weird issue with vue-router v4 and will be fixed in v5 (vue 3) + return next({ + ...to, + query: { + ...to.query, + query: searchStore.query, + }, + }) + } + } + next() +}) + +/** + * Are we currently on the search view. + * Needed to disable the action menu (we cannot change the search mode there) + */ +const isSearchView = computed(() => currentView.value.id === VIEW_ID) + +/** + * Local search is only possible on real DAV resources within the files root + */ +const canSearchLocally = computed(() => { + if (searchStore.base) { + return true + } + + const folder = filesStore.getDirectoryByPath(currentView.value.id, directory.value) + return folder?.isDavResource && folder?.root?.startsWith('/files/') +}) + +/** + * Different searchbox label depending if filtering or searching + */ +const searchLabel = computed(() => { + if (searchStore.scope === 'globally') { + return t('files', 'Search globally by filename …') + } else if (searchStore.scope === 'locally') { + return t('files', 'Search here by filename …') + } + return t('files', 'Filter file names …') +}) + +/** + * Update the search value and set the base if needed + * @param value - The new value + */ +function onUpdateSearch(value: string) { + if (searchStore.scope === 'locally' && currentView.value.id !== VIEW_ID) { + searchStore.base = filesStore.getDirectoryByPath(currentView.value.id, directory.value) + } + searchStore.query = value +} +</script> + +<template> + <NcAppNavigationSearch :label="searchLabel" :model-value="searchStore.query" @update:modelValue="onUpdateSearch"> + <template #actions> + <NcActions :aria-label="t('files', 'Search scope options')" :disabled="isSearchView"> + <template #icon> + <NcIconSvgWrapper :path="searchStore.scope === 'globally' ? mdiSearchWeb : mdiMagnify" /> + </template> + <NcActionButton close-after-click @click="searchStore.scope = 'filter'"> + <template #icon> + <NcIconSvgWrapper :path="mdiMagnify" /> + </template> + {{ t('files', 'Filter in current view') }} + </NcActionButton> + <NcActionButton v-if="canSearchLocally" close-after-click @click="searchStore.scope = 'locally'"> + <template #icon> + <NcIconSvgWrapper :path="mdiMagnify" /> + </template> + {{ t('files', 'Search from this location') }} + </NcActionButton> + <NcActionButton close-after-click @click="searchStore.scope = 'globally'"> + <template #icon> + <NcIconSvgWrapper :path="mdiSearchWeb" /> + </template> + {{ t('files', 'Search globally') }} + </NcActionButton> + </NcActions> + </template> + </NcAppNavigationSearch> +</template> diff --git a/apps/files/src/composables/useBeforeNavigation.ts b/apps/files/src/composables/useBeforeNavigation.ts new file mode 100644 index 00000000000..38b72e40fb3 --- /dev/null +++ b/apps/files/src/composables/useBeforeNavigation.ts @@ -0,0 +1,20 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { NavigationGuard } from 'vue-router' + +import { onUnmounted } from 'vue' +import { useRouter } from 'vue-router/composables' + +/** + * Helper until we use Vue-Router v4 (Vue3). + * + * @param fn - The navigation guard + */ +export function onBeforeNavigation(fn: NavigationGuard) { + const router = useRouter() + const remove = router.beforeResolve(fn) + onUnmounted(remove) +} diff --git a/apps/files/src/composables/useFilenameFilter.ts b/apps/files/src/composables/useFilenameFilter.ts deleted file mode 100644 index 54c16f35384..00000000000 --- a/apps/files/src/composables/useFilenameFilter.ts +++ /dev/null @@ -1,47 +0,0 @@ -/*! - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { registerFileListFilter, unregisterFileListFilter } from '@nextcloud/files' -import { watchThrottled } from '@vueuse/core' -import { onMounted, onUnmounted, ref } from 'vue' -import { FilenameFilter } from '../filters/FilenameFilter' - -/** - * This is for the `Navigation` component to provide a filename filter - */ -export function useFilenameFilter() { - const searchQuery = ref('') - const filenameFilter = new FilenameFilter() - - /** - * Updating the search query ref from the filter - * @param event The update:query event - */ - function updateQuery(event: CustomEvent) { - if (event.type === 'update:query') { - searchQuery.value = event.detail - event.stopPropagation() - } - } - - onMounted(() => { - filenameFilter.addEventListener('update:query', updateQuery) - registerFileListFilter(filenameFilter) - }) - onUnmounted(() => { - filenameFilter.removeEventListener('update:query', updateQuery) - unregisterFileListFilter(filenameFilter.id) - }) - - // Update the query on the filter, but throttle to max. every 800ms - // This will debounce the filter refresh - watchThrottled(searchQuery, () => { - filenameFilter.updateQuery(searchQuery.value) - }, { throttle: 800 }) - - return { - searchQuery, - } -} diff --git a/apps/files/src/eventbus.d.ts b/apps/files/src/eventbus.d.ts index fb61b4a6d03..ab8dbb63dfc 100644 --- a/apps/files/src/eventbus.d.ts +++ b/apps/files/src/eventbus.d.ts @@ -2,7 +2,9 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { IFileListFilter, Node } from '@nextcloud/files' + +import type { IFileListFilter, Node, View } from '@nextcloud/files' +import type { SearchScope } from './types' declare module '@nextcloud/event-bus' { export interface NextcloudEvents { @@ -13,8 +15,13 @@ declare module '@nextcloud/event-bus' { 'files:favorites:removed': Node 'files:favorites:added': Node + 'files:filter:added': IFileListFilter + 'files:filter:removed': string + // the state of some filters has changed 'files:filters:changed': undefined + 'files:navigation:changed': View + 'files:node:created': Node 'files:node:deleted': Node 'files:node:updated': Node @@ -22,8 +29,7 @@ declare module '@nextcloud/event-bus' { 'files:node:renamed': Node 'files:node:moved': { node: Node, oldSource: string } - 'files:filter:added': IFileListFilter - 'files:filter:removed': string + 'files:search:updated': { query: string, scope: SearchScope } } } diff --git a/apps/files/src/filters/FilenameFilter.ts b/apps/files/src/filters/FilenameFilter.ts index 5019ca42d83..7914142f6ca 100644 --- a/apps/files/src/filters/FilenameFilter.ts +++ b/apps/files/src/filters/FilenameFilter.ts @@ -4,17 +4,31 @@ */ import type { IFileListFilterChip, INode } from '@nextcloud/files' -import { FileListFilter } from '@nextcloud/files' + +import { subscribe } from '@nextcloud/event-bus' +import { FileListFilter, registerFileListFilter } from '@nextcloud/files' + +/** + * Register the filename filter + */ +export function registerFilenameFilter() { + registerFileListFilter(new FilenameFilter()) +} /** * Simple file list filter controlled by the Navigation search box */ -export class FilenameFilter extends FileListFilter { +class FilenameFilter extends FileListFilter { private searchQuery = '' constructor() { super('files:filename', 5) + subscribe('files:search:updated', ({ query, scope }) => { + if (scope === 'filter') { + this.updateQuery(query) + } + }) } public filter(nodes: INode[]): INode[] { diff --git a/apps/files/src/init.ts b/apps/files/src/init.ts index 492ffbb1915..a9aedb5fb63 100644 --- a/apps/files/src/init.ts +++ b/apps/files/src/init.ts @@ -26,13 +26,16 @@ import { registerTemplateEntries } from './newMenu/newFromTemplate.ts' import { registerFavoritesView } from './views/favorites.ts' import registerRecentView from './views/recent' import registerPersonalFilesView from './views/personal-files' -import registerFilesView from './views/files' +import { registerFilesView } from './views/files' import { registerFolderTreeView } from './views/folderTree.ts' +import { registerSearchView } from './views/search.ts' + import registerPreviewServiceWorker from './services/ServiceWorker.js' import { initLivePhotos } from './services/LivePhotos' import { isPublicShare } from '@nextcloud/sharing/public' import { registerConvertActions } from './actions/convertAction.ts' +import { registerFilenameFilter } from './filters/FilenameFilter.ts' // Register file actions registerConvertActions() @@ -56,8 +59,9 @@ registerTemplateEntries() if (isPublicShare() === false) { registerFavoritesView() registerFilesView() - registerRecentView() registerPersonalFilesView() + registerRecentView() + registerSearchView() registerFolderTreeView() } @@ -65,6 +69,7 @@ if (isPublicShare() === false) { registerHiddenFilesFilter() registerTypeFilter() registerModifiedFilter() +registerFilenameFilter() // Register preview service worker registerPreviewServiceWorker() diff --git a/apps/files/src/router/router.ts b/apps/files/src/router/router.ts index 00f08c38d31..20c252d6954 100644 --- a/apps/files/src/router/router.ts +++ b/apps/files/src/router/router.ts @@ -11,7 +11,6 @@ import Router, { isNavigationFailure, NavigationFailureType } from 'vue-router' import Vue from 'vue' import { useFilesStore } from '../store/files' -import { useNavigation } from '../composables/useNavigation' import { usePathsStore } from '../store/paths' import logger from '../logger' @@ -74,14 +73,27 @@ const router = new Router({ }, }) +// Handle aborted navigation (NavigationGuards) gracefully +router.onError((error) => { + if (isNavigationFailure(error, NavigationFailureType.aborted)) { + logger.debug('Navigation was aboorted', { error }) + } else { + throw error + } +}) + // If navigating back from a folder to a parent folder, // we need to keep the current dir fileid so it's highlighted // and scrolled into view. -router.beforeEach((to, from, next) => { +router.beforeResolve((to, from, next) => { if (to.params?.parentIntercept) { delete to.params.parentIntercept - next() - return + return next() + } + + if (to.params.view !== from.params.view) { + // skip if different views + return next() } const fromDir = (from.query?.dir || '/') as string @@ -89,17 +101,16 @@ router.beforeEach((to, from, next) => { // We are going back to a parent directory if (relative(fromDir, toDir) === '..') { - const { currentView } = useNavigation() const { getNode } = useFilesStore() const { getPath } = usePathsStore() - if (!currentView.value?.id) { + if (!from.params.view) { logger.error('No current view id found, cannot navigate to parent directory', { fromDir, toDir }) return next() } // Get the previous parent's file id - const fromSource = getPath(currentView.value?.id, fromDir) + const fromSource = getPath(from.params.view, fromDir) if (!fromSource) { logger.error('No source found for the parent directory', { fromDir, toDir }) return next() @@ -112,7 +123,7 @@ router.beforeEach((to, from, next) => { } logger.debug('Navigating back to parent directory', { fromDir, toDir, fileId }) - next({ + return next({ name: 'filelist', query: to.query, params: { diff --git a/apps/files/src/services/Search.spec.ts b/apps/files/src/services/Search.spec.ts new file mode 100644 index 00000000000..c2840521a15 --- /dev/null +++ b/apps/files/src/services/Search.spec.ts @@ -0,0 +1,61 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createPinia, setActivePinia } from 'pinia' +import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { getContents } from './Search.ts' +import { Folder, Permission } from '@nextcloud/files' + +const searchNodes = vi.hoisted(() => vi.fn()) +vi.mock('./WebDavSearch.ts', () => ({ searchNodes })) +vi.mock('@nextcloud/auth') + +describe('Search service', () => { + const fakeFolder = new Folder({ owner: 'owner', source: 'https://cloud.example.com/remote.php/dav/files/owner/folder', root: '/files/owner' }) + + beforeAll(() => { + window.OCP ??= {} + window.OCP.Files ??= {} + window.OCP.Files.Router ??= { params: {}, query: {} } + vi.spyOn(window.OCP.Files.Router, 'params', 'get').mockReturnValue({ view: 'files' }) + }) + + beforeEach(() => { + vi.restoreAllMocks() + setActivePinia(createPinia()) + }) + + it('rejects on error', async () => { + searchNodes.mockImplementationOnce(() => { throw new Error('expected error') }) + expect(getContents).rejects.toThrow('expected error') + }) + + it('returns the search results and a fake root', async () => { + searchNodes.mockImplementationOnce(() => [fakeFolder]) + const { contents, folder } = await getContents() + + expect(searchNodes).toHaveBeenCalledOnce() + expect(contents).toHaveLength(1) + expect(contents).toEqual([fakeFolder]) + // read only root + expect(folder.permissions).toBe(Permission.READ) + }) + + it('can be cancelled', async () => { + const { promise, resolve } = Promise.withResolvers<Event>() + searchNodes.mockImplementationOnce(async (_, { signal }: { signal: AbortSignal}) => { + signal.addEventListener('abort', resolve) + await promise + return [] + }) + + const content = getContents() + content.cancel() + + // its cancelled thus the promise returns the event + const event = await promise + expect(event.type).toBe('abort') + }) +}) diff --git a/apps/files/src/services/Search.ts b/apps/files/src/services/Search.ts new file mode 100644 index 00000000000..ae6f1ee50e0 --- /dev/null +++ b/apps/files/src/services/Search.ts @@ -0,0 +1,44 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { ContentsWithRoot } from '@nextcloud/files' + +import { getCurrentUser } from '@nextcloud/auth' +import { Folder, Permission } from '@nextcloud/files' +import { defaultRemoteURL } from '@nextcloud/files/dav' +import { CancelablePromise } from 'cancelable-promise' +import { searchNodes } from './WebDavSearch.ts' +import logger from '../logger.ts' +import { useSearchStore } from '../store/search.ts' +import { getPinia } from '../store/index.ts' + +/** + * Get the contents for a search view + */ +export function getContents(): CancelablePromise<ContentsWithRoot> { + const controller = new AbortController() + + const searchStore = useSearchStore(getPinia()) + const dir = searchStore.base?.path + + return new CancelablePromise<ContentsWithRoot>(async (resolve, reject, cancel) => { + cancel(() => controller.abort()) + try { + const contents = await searchNodes(searchStore.query, { dir, signal: controller.signal }) + resolve({ + contents, + folder: new Folder({ + id: 0, + source: `${defaultRemoteURL}#search`, + owner: getCurrentUser()!.uid, + permissions: Permission.READ, + }), + }) + } catch (error) { + logger.error('Failed to fetch search results', { error }) + reject(error) + } + }) +} diff --git a/apps/files/src/services/WebDavSearch.ts b/apps/files/src/services/WebDavSearch.ts new file mode 100644 index 00000000000..feb7f30b357 --- /dev/null +++ b/apps/files/src/services/WebDavSearch.ts @@ -0,0 +1,83 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { INode } from '@nextcloud/files' +import type { ResponseDataDetailed, SearchResult } from 'webdav' + +import { getCurrentUser } from '@nextcloud/auth' +import { defaultRootPath, getDavNameSpaces, getDavProperties, resultToNode } from '@nextcloud/files/dav' +import { getBaseUrl } from '@nextcloud/router' +import { client } from './WebdavClient.ts' +import logger from '../logger.ts' + +export interface SearchNodesOptions { + dir?: string, + signal?: AbortSignal +} + +/** + * Search for nodes matching the given query. + * + * @param query - Search query + * @param options - Options + * @param options.dir - The base directory to scope the search to + * @param options.signal - Abort signal for the request + */ +export async function searchNodes(query: string, { dir, signal }: SearchNodesOptions): Promise<INode[]> { + const user = getCurrentUser() + if (!user) { + // the search plugin only works for user roots + return [] + } + + query = query.trim() + if (query.length < 3) { + // the search plugin only works with queries of at least 3 characters + return [] + } + + if (dir && !dir.startsWith('/')) { + dir = `/${dir}` + } + + logger.debug('Searching for nodes', { query, dir }) + const { data } = await client.search('/', { + details: true, + signal, + data: ` +<d:searchrequest ${getDavNameSpaces()}> + <d:basicsearch> + <d:select> + <d:prop> + ${getDavProperties()} + </d:prop> + </d:select> + <d:from> + <d:scope> + <d:href>/files/${user.uid}${dir || ''}</d:href> + <d:depth>infinity</d:depth> + </d:scope> + </d:from> + <d:where> + <d:like> + <d:prop> + <d:displayname/> + </d:prop> + <d:literal>%${query.replace('%', '')}%</d:literal> + </d:like> + </d:where> + <d:orderby/> + </d:basicsearch> +</d:searchrequest>`, + }) as ResponseDataDetailed<SearchResult> + + // check if the request was aborted + if (signal?.aborted) { + return [] + } + + // otherwise return the result mapped to Nextcloud nodes + return data.results.map((result) => resultToNode(result, defaultRootPath, getBaseUrl())) +} diff --git a/apps/files/src/store/files.ts b/apps/files/src/store/files.ts index 295704c880b..3591832d0c4 100644 --- a/apps/files/src/store/files.ts +++ b/apps/files/src/store/files.ts @@ -54,13 +54,13 @@ export const useFilesStore = function(...args) { actions: { /** - * Get cached child nodes within a given path + * Get cached directory matching a given path * - * @param service The service (files view) - * @param path The path relative within the service - * @return Array of cached nodes within the path + * @param service - The service (files view) + * @param path - The path relative within the service + * @return The folder if found */ - getNodesByPath(service: string, path?: string): Node[] { + getDirectoryByPath(service: string, path?: string): Folder | undefined { const pathsStore = usePathsStore() let folder: Folder | undefined @@ -74,6 +74,19 @@ export const useFilesStore = function(...args) { } } + return folder + }, + + /** + * Get cached child nodes within a given path + * + * @param service - The service (files view) + * @param path - The path relative within the service + * @return Array of cached nodes within the path + */ + getNodesByPath(service: string, path?: string): Node[] { + const folder = this.getDirectoryByPath(service, path) + // If we found a cache entry and the cache entry was already loaded (has children) then use it return (folder?._children ?? []) .map((source: string) => this.getNode(source)) diff --git a/apps/files/src/store/search.ts b/apps/files/src/store/search.ts new file mode 100644 index 00000000000..286cad253fc --- /dev/null +++ b/apps/files/src/store/search.ts @@ -0,0 +1,170 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { INode, View } from '@nextcloud/files' +import type RouterService from '../services/RouterService' +import type { SearchScope } from '../types' + +import { emit, subscribe } from '@nextcloud/event-bus' +import { defineStore } from 'pinia' +import { ref, watch } from 'vue' +import { VIEW_ID } from '../views/search' +import logger from '../logger' +import debounce from 'debounce' + +export const useSearchStore = defineStore('search', () => { + /** + * The current search query + */ + const query = ref('') + + /** + * Where to start the search + */ + const base = ref<INode>() + + /** + * Scope of the search. + * Scopes: + * - filter: only filter current file list + * - locally: search from current location recursivly + * - globally: search everywhere + */ + const scope = ref<SearchScope>('filter') + + // reset the base if query is cleared + watch(scope, () => { + if (scope.value !== 'locally') { + base.value = undefined + } + + updateSearch() + }) + + watch(query, (old, current) => { + // skip if only whitespaces changed + if (old.trim() === current.trim()) { + return + } + + updateSearch() + }) + + // initialize the search store + initialize() + + /** + * Debounced update of the current route + * @private + */ + const updateRouter = debounce((isSearch: boolean, fileid?: number) => { + const router = window.OCP.Files.Router as RouterService + router.goToRoute( + undefined, + { + view: VIEW_ID, + ...(fileid === undefined ? {} : { fileid: String(fileid) }), + }, + { + query: query.value, + }, + isSearch, + ) + }) + + /** + * Handle updating the filter if needed. + * Also update the search view by updating the current route if needed. + * + * @private + */ + function updateSearch() { + // emit the search event to update the filter + emit('files:search:updated', { query: query.value, scope: scope.value }) + + const router = window.OCP.Files.Router as RouterService + + // if we are on the search view and the query was unset or scope was set to 'filter' we need to move back to the files view + if (router.params.view === VIEW_ID && (query.value === '' || scope.value === 'filter')) { + scope.value = 'filter' + return router.goToRoute( + undefined, + { + view: 'files', + }, + { + ...router.query, + query: undefined, + }, + ) + } + + // for the filter scope we do not need to adjust the current route anymore + // also if the query is empty we do not need to do anything + if (scope.value === 'filter' || !query.value) { + return + } + + // we only use the directory if we search locally + const fileid = scope.value === 'locally' ? base.value?.fileid : undefined + const isSearch = router.params.view === VIEW_ID + + logger.debug('Update route for updated search query', { query: query.value, fileid, isSearch }) + updateRouter(isSearch, fileid) + } + + /** + * Event handler that resets the store if the file list view was changed. + * + * @param view - The new view that is active + * @private + */ + function onViewChanged(view: View) { + if (view.id !== VIEW_ID) { + query.value = '' + scope.value = 'filter' + } + } + + /** + * Initialize the store from the router if needed + */ + function initialize() { + subscribe('files:navigation:changed', onViewChanged) + + const router = window.OCP.Files.Router as RouterService + // if we initially load the search view (e.g. hard page refresh) + // then we need to initialize the store from the router + if (router.params.view === VIEW_ID) { + query.value = [router.query.query].flat()[0] ?? '' + + if (query.value) { + scope.value = 'globally' + logger.debug('Directly navigated to search view', { query: query.value }) + } else { + // we do not have any query so we need to move to the files list + logger.info('Directly navigated to search view without any query, redirect to files view.') + router.goToRoute( + undefined, + { + ...router.params, + view: 'files', + }, + { + ...router.query, + query: undefined, + }, + true, + ) + } + } + } + + return { + base, + query, + scope, + } +}) diff --git a/apps/files/src/types.ts b/apps/files/src/types.ts index db3de13d4eb..7e9696d31d6 100644 --- a/apps/files/src/types.ts +++ b/apps/files/src/types.ts @@ -111,6 +111,11 @@ export interface ActiveStore { activeAction: FileAction|null } +/** + * Search scope for the in-files-search + */ +export type SearchScope = 'filter'|'locally'|'globally' + export interface TemplateFile { app: string label: string diff --git a/apps/files/src/views/FilesList.vue b/apps/files/src/views/FilesList.vue index 60791a2b527..89d9fed6ce5 100644 --- a/apps/files/src/views/FilesList.vue +++ b/apps/files/src/views/FilesList.vue @@ -160,6 +160,7 @@ import { showError, showSuccess, showWarning } from '@nextcloud/dialogs' import { ShareType } from '@nextcloud/sharing' import { UploadPicker, UploadStatus } from '@nextcloud/upload' import { loadState } from '@nextcloud/initial-state' +import { useThrottleFn } from '@vueuse/core' import { defineComponent } from 'vue' import NcAppContent from '@nextcloud/vue/components/NcAppContent' @@ -325,16 +326,7 @@ export default defineComponent({ return } - if (this.directory === '/') { - return this.filesStore.getRoot(this.currentView.id) - } - - const source = this.pathsStore.getPath(this.currentView.id, this.directory) - if (source === undefined) { - return - } - - return this.filesStore.getNode(source) as Folder + return this.filesStore.getDirectoryByPath(this.currentView.id, this.directory) }, dirContents(): Node[] { @@ -479,6 +471,10 @@ export default defineComponent({ const hidden = this.dirContents.length - this.dirContentsFiltered.length return getSummaryFor(this.dirContentsFiltered, hidden) }, + + debouncedFetchContent() { + return useThrottleFn(this.fetchContent, 800, true) + }, }, watch: { @@ -540,14 +536,16 @@ export default defineComponent({ // filter content if filter were changed subscribe('files:filters:changed', this.filterDirContent) + subscribe('files:search:updated', this.onUpdateSearch) + // Finally, fetch the current directory contents await this.fetchContent() if (this.fileId) { // If we have a fileId, let's check if the file exists - const node = this.dirContents.find(node => node.fileid.toString() === this.fileId.toString()) + const node = this.dirContents.find(node => node.fileid?.toString() === this.fileId?.toString()) // If the file isn't in the current directory nor if // the current directory is the file, we show an error - if (!node && this.currentFolder.fileid.toString() !== this.fileId.toString()) { + if (!node && this.currentFolder?.fileid?.toString() !== this.fileId.toString()) { showError(t('files', 'The file could not be found')) } } @@ -557,9 +555,17 @@ export default defineComponent({ unsubscribe('files:node:deleted', this.onNodeDeleted) unsubscribe('files:node:updated', this.onUpdatedNode) unsubscribe('files:config:updated', this.fetchContent) + unsubscribe('files:filters:changed', this.filterDirContent) + unsubscribe('files:search:updated', this.onUpdateSearch) }, methods: { + onUpdateSearch({ query, scope }) { + if (query && scope !== 'filter') { + this.debouncedFetchContent() + } + }, + async fetchContent() { this.loading = true this.error = null diff --git a/apps/files/src/views/Navigation.cy.ts b/apps/files/src/views/Navigation.cy.ts index a88878e2d3a..6b03efa4f5f 100644 --- a/apps/files/src/views/Navigation.cy.ts +++ b/apps/files/src/views/Navigation.cy.ts @@ -10,7 +10,8 @@ import NavigationView from './Navigation.vue' import { useViewConfigStore } from '../store/viewConfig' import { Folder, View, getNavigation } from '@nextcloud/files' -import router from '../router/router' +import router from '../router/router.ts' +import RouterService from '../services/RouterService' const resetNavigation = () => { const nav = getNavigation() @@ -27,9 +28,18 @@ const createView = (id: string, name: string, parent?: string) => new View({ parent, }) +function mockWindow() { + window.OCP ??= {} + window.OCP.Files ??= {} + window.OCP.Files.Router = new RouterService(router) +} + describe('Navigation renders', () => { - before(() => { + before(async () => { delete window._nc_navigation + mockWindow() + getNavigation().register(createView('files', 'Files')) + await router.replace({ name: 'filelist', params: { view: 'files' } }) cy.mockInitialState('files', 'storageStats', { used: 1000 * 1000 * 1000, @@ -41,6 +51,7 @@ describe('Navigation renders', () => { it('renders', () => { cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -60,6 +71,7 @@ describe('Navigation API', () => { before(async () => { delete window._nc_navigation Navigation = getNavigation() + mockWindow() await router.replace({ name: 'filelist', params: { view: 'files' } }) }) @@ -152,14 +164,18 @@ describe('Navigation API', () => { }) describe('Quota rendering', () => { - before(() => { + before(async () => { delete window._nc_navigation + mockWindow() + getNavigation().register(createView('files', 'Files')) + await router.replace({ name: 'filelist', params: { view: 'files' } }) }) afterEach(() => cy.unmockInitialState()) it('Unknown quota', () => { cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -177,6 +193,7 @@ describe('Quota rendering', () => { }) cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -197,6 +214,7 @@ describe('Quota rendering', () => { }) cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, @@ -219,6 +237,7 @@ describe('Quota rendering', () => { }) cy.mount(NavigationView, { + router, global: { plugins: [createTestingPinia({ createSpy: cy.spy, diff --git a/apps/files/src/views/Navigation.vue b/apps/files/src/views/Navigation.vue index 3147268f34d..c424a0d74b8 100644 --- a/apps/files/src/views/Navigation.vue +++ b/apps/files/src/views/Navigation.vue @@ -7,7 +7,7 @@ class="files-navigation" :aria-label="t('files', 'Files')"> <template #search> - <NcAppNavigationSearch v-model="searchQuery" :label="t('files', 'Filter file names …')" /> + <FilesNavigationSearch /> </template> <template #default> <NcAppNavigationList class="files-navigation__list" @@ -39,24 +39,24 @@ </template> <script lang="ts"> -import { getNavigation, type View } from '@nextcloud/files' +import type { View } from '@nextcloud/files' import type { ViewConfig } from '../types.ts' -import { defineComponent } from 'vue' import { emit, subscribe } from '@nextcloud/event-bus' -import { translate as t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n' +import { getNavigation } from '@nextcloud/files' +import { t, getCanonicalLocale, getLanguage } from '@nextcloud/l10n' +import { defineComponent } from 'vue' import IconCog from 'vue-material-design-icons/Cog.vue' import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation' import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem' import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList' -import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSearch' import NavigationQuota from '../components/NavigationQuota.vue' import SettingsModal from './Settings.vue' import FilesNavigationItem from '../components/FilesNavigationItem.vue' +import FilesNavigationSearch from '../components/FilesNavigationSearch.vue' import { useNavigation } from '../composables/useNavigation' -import { useFilenameFilter } from '../composables/useFilenameFilter' import { useFiltersStore } from '../store/filters.ts' import { useViewConfigStore } from '../store/viewConfig.ts' import logger from '../logger.ts' @@ -75,12 +75,12 @@ export default defineComponent({ components: { IconCog, FilesNavigationItem, + FilesNavigationSearch, NavigationQuota, NcAppNavigation, NcAppNavigationItem, NcAppNavigationList, - NcAppNavigationSearch, SettingsModal, }, @@ -88,11 +88,9 @@ export default defineComponent({ const filtersStore = useFiltersStore() const viewConfigStore = useViewConfigStore() const { currentView, views } = useNavigation() - const { searchQuery } = useFilenameFilter() return { currentView, - searchQuery, t, views, diff --git a/apps/files/src/views/SearchEmptyView.vue b/apps/files/src/views/SearchEmptyView.vue new file mode 100644 index 00000000000..0553e416caf --- /dev/null +++ b/apps/files/src/views/SearchEmptyView.vue @@ -0,0 +1,57 @@ +<!-- + - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<script setup lang="ts"> +import { mdiMagnifyClose } from '@mdi/js' +import { t } from '@nextcloud/l10n' +import debounce from 'debounce' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcInputField from '@nextcloud/vue/components/NcInputField' +import { getPinia } from '../store/index.ts' +import { useSearchStore } from '../store/search.ts' + +const searchStore = useSearchStore(getPinia()) +const debouncedUpdate = debounce((value: string) => { + searchStore.query = value +}, 500) +</script> + +<template> + <NcEmptyContent :name="t('files', 'No search results for “{query}”', { query: searchStore.query })"> + <template #icon> + <NcIconSvgWrapper :path="mdiMagnifyClose" /> + </template> + <template #action> + <div class="search-empty-view__wrapper"> + <NcInputField class="search-empty-view__input" + :label="t('files', 'Search for files')" + :model-value="searchStore.query" + type="search" + @update:model-value="debouncedUpdate" /> + <NcButton v-if="searchStore.scope === 'locally'" @click="searchStore.scope = 'globally'"> + {{ t('files', 'Search globally') }} + </NcButton> + </div> + </template> + </NcEmptyContent> +</template> + +<style scoped lang="scss"> +.search-empty-view { + &__input { + flex: 0 1; + min-width: min(400px, 50vw); + } + + &__wrapper { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: baseline; + } +} +</style> diff --git a/apps/files/src/views/files.ts b/apps/files/src/views/files.ts index a49a13f91e1..699e173de63 100644 --- a/apps/files/src/views/files.ts +++ b/apps/files/src/views/files.ts @@ -8,10 +8,15 @@ import FolderSvg from '@mdi/svg/svg/folder.svg?raw' import { getContents } from '../services/Files' import { View, getNavigation } from '@nextcloud/files' -export default () => { +export const VIEW_ID = 'files' + +/** + * Register the files view to the navigation + */ +export function registerFilesView() { const Navigation = getNavigation() Navigation.register(new View({ - id: 'files', + id: VIEW_ID, name: t('files', 'All files'), caption: t('files', 'List of your files and folders.'), diff --git a/apps/files/src/views/search.ts b/apps/files/src/views/search.ts new file mode 100644 index 00000000000..a30f732163c --- /dev/null +++ b/apps/files/src/views/search.ts @@ -0,0 +1,51 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { ComponentPublicInstanceConstructor } from 'vue/types/v3-component-public-instance' + +import { View, getNavigation } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' +import { getContents } from '../services/Search.ts' +import { VIEW_ID as FILES_VIEW_ID } from './files.ts' +import MagnifySvg from '@mdi/svg/svg/magnify.svg?raw' +import Vue from 'vue' + +export const VIEW_ID = 'search' + +/** + * Register the search-in-files view + */ +export function registerSearchView() { + let instance: Vue + let view: ComponentPublicInstanceConstructor + + const Navigation = getNavigation() + Navigation.register(new View({ + id: VIEW_ID, + name: t('files', 'Search'), + caption: t('files', 'Search results within your files.'), + + async emptyView(el) { + if (!view) { + view = (await import('./SearchEmptyView.vue')).default + } else { + instance.$destroy() + } + instance = new Vue(view) + instance.$mount(el) + }, + + icon: MagnifySvg, + order: 10, + + parent: FILES_VIEW_ID, + // it should be shown expanded + expanded: true, + // this view is hidden by default and only shown when active + hidden: true, + + getContents, + })) +} diff --git a/apps/files/templates/index.php b/apps/files/templates/index.php index 9ba105e240d..5281220282a 100644 --- a/apps/files/templates/index.php +++ b/apps/files/templates/index.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/tests/Controller/ApiControllerTest.php b/apps/files/tests/Controller/ApiControllerTest.php index 0c9d7a4fa6e..d6d86e293fd 100644 --- a/apps/files/tests/Controller/ApiControllerTest.php +++ b/apps/files/tests/Controller/ApiControllerTest.php @@ -29,7 +29,6 @@ use OCP\IPreview; use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; -use OCP\Share\IAttributes; use OCP\Share\IManager; use OCP\Share\IShare; use PHPUnit\Framework\MockObject\MockObject; @@ -183,16 +182,10 @@ class ApiControllerTest extends TestCase { } public function testGetThumbnailSharedNoDownload(): void { - $attributes = $this->createMock(IAttributes::class); - $attributes->expects(self::once()) - ->method('getAttribute') - ->with('permissions', 'download') - ->willReturn(false); - $share = $this->createMock(IShare::class); $share->expects(self::once()) - ->method('getAttributes') - ->willReturn($attributes); + ->method('canSeeContent') + ->willReturn(false); $storage = $this->createMock(ISharedStorage::class); $storage->expects(self::once()) @@ -221,8 +214,8 @@ class ApiControllerTest extends TestCase { public function testGetThumbnailShared(): void { $share = $this->createMock(IShare::class); $share->expects(self::once()) - ->method('getAttributes') - ->willReturn(null); + ->method('canSeeContent') + ->willReturn(true); $storage = $this->createMock(ISharedStorage::class); $storage->expects(self::once()) |