Merge branch 'stable27' into release/27.1.0

Signed-off-by: John Molakvoæ <skjnldsv@users.noreply.github.com>
This commit is contained in:
John Molakvoæ 2023-08-11 09:06:46 +02:00 committed by GitHub
commit cc97d03b72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
389 changed files with 9672 additions and 584 deletions

1
.github/CODEOWNERS vendored
View file

@ -11,6 +11,7 @@
/apps/federation/appinfo/info.xml @datenangebot
/apps/files/appinfo/info.xml @skjnldsv @Pytal @ArtificialOwl @come-nc @artonge @icewind1991 @szaimen @susnux @Fenn-CS
/apps/files_external/appinfo/info.xml @icewind1991 @artonge
/apps/files_reminders/appinfo/info.xml @Pytal
/apps/files_sharing/appinfo/info.xml @skjnldsv @come-nc
/apps/files_trashbin/appinfo/info.xml @Pytal @icewind1991
/apps/files_versions/appinfo/info.xml @artonge @icewind1991

1
.gitignore vendored
View file

@ -24,6 +24,7 @@
!/apps/sharebymail
!/apps/encryption
!/apps/files_external
!/apps/files_reminders
!/apps/files_sharing
!/apps/files_trashbin
!/apps/files_versions

@ -1 +1 @@
Subproject commit 216b791c9081f06a4ed6581a436020647dd41bbd
Subproject commit 6f18457f7dfcf23ce2877495a9323a144fd4a166

View file

@ -0,0 +1,8 @@
OC.L10N.register(
"cloud_federation_api",
{
"Cloud Federation API" : "Cloud Federation API",
"Enable clouds to communicate with each other and exchange data" : "Cho phép các đám mây giao tiếp với nhau và trao đổi dữ liệu",
"The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API Cloud Federation cho phép các phiên bản Nextcloud khác nhau giao tiếp với nhau và trao đổi dữ liệu."
},
"nplurals=1; plural=0;");

View file

@ -0,0 +1,6 @@
{ "translations": {
"Cloud Federation API" : "Cloud Federation API",
"Enable clouds to communicate with each other and exchange data" : "Cho phép các đám mây giao tiếp với nhau và trao đổi dữ liệu",
"The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data." : "API Cloud Federation cho phép các phiên bản Nextcloud khác nhau giao tiếp với nhau và trao đổi dữ liệu."
},"pluralForm" :"nplurals=1; plural=0;"
}

View file

@ -1,7 +1,7 @@
OC.L10N.register(
"comments",
{
"Comments" : "Các bình luận",
"Comments" : "Bình luận",
"You commented" : "Bạn đã bình luận",
"{author} commented" : "{author} đã bình luận",
"You commented on %1$s" : "Bạn đã bình luận về %1$s",
@ -19,6 +19,7 @@ OC.L10N.register(
"No comments yet, start the conversation!" : "Không có bình luận nào, bắt đầu cuộc hội thoại!",
"No more messages" : "Không có thêm tin nhắn",
"Retry" : "Thử lại",
"Failed to mark comments as read" : "Không thể đánh dấu bình luận là đã đọc",
"Unable to load the comments list" : "Không thể tải danh sách bình luận",
"_%n unread comment_::_%n unread comments_" : ["%n bình luận chưa đọc"],
"_1 new comment_::_{unread} new comments_" : ["{unread} bình luận mới"],

View file

@ -1,5 +1,5 @@
{ "translations": {
"Comments" : "Các bình luận",
"Comments" : "Bình luận",
"You commented" : "Bạn đã bình luận",
"{author} commented" : "{author} đã bình luận",
"You commented on %1$s" : "Bạn đã bình luận về %1$s",
@ -17,6 +17,7 @@
"No comments yet, start the conversation!" : "Không có bình luận nào, bắt đầu cuộc hội thoại!",
"No more messages" : "Không có thêm tin nhắn",
"Retry" : "Thử lại",
"Failed to mark comments as read" : "Không thể đánh dấu bình luận là đã đọc",
"Unable to load the comments list" : "Không thể tải danh sách bình luận",
"_%n unread comment_::_%n unread comments_" : ["%n bình luận chưa đọc"],
"_1 new comment_::_{unread} new comments_" : ["{unread} bình luận mới"],

View file

@ -0,0 +1,9 @@
OC.L10N.register(
"contactsinteraction",
{
"Recently contacted" : "Liên hệ gần đây",
"Contacts Interaction" : "Liên hệ tương tác",
"Manages interaction between users and contacts" : "Quản lý tương tác giữa người dùng và liên hệ",
"Collect data about user and contacts interactions and provide an address book for the data" : "Thu thập dữ liệu về tương tác của người dùng và danh bạ, đồng thời cung cấp sổ địa chỉ cho dữ liệu"
},
"nplurals=1; plural=0;");

View file

@ -0,0 +1,7 @@
{ "translations": {
"Recently contacted" : "Liên hệ gần đây",
"Contacts Interaction" : "Liên hệ tương tác",
"Manages interaction between users and contacts" : "Quản lý tương tác giữa người dùng và liên hệ",
"Collect data about user and contacts interactions and provide an address book for the data" : "Thu thập dữ liệu về tương tác của người dùng và danh bạ, đồng thời cung cấp sổ địa chỉ cho dữ liệu"
},"pluralForm" :"nplurals=1; plural=0;"
}

View file

@ -3,6 +3,8 @@ OC.L10N.register(
{
"Dashboard" : "Dashboard",
"Dashboard app" : "Dashboard app",
"Start your day informed\n\nThe Nextcloud Dashboard is your starting point of the day, giving you an overview of your upcoming appointments, urgent emails, chat messages, incoming tickets, latest tweets and much more! Users can add the widgets they like and change the background to their liking." : "Start din dag informeret\n\nNextcloud Dashboard er dit udgangspunkt på dagen og giver dig en oversigt over dine kommende aftaler, hastemails, chatbeskeder, indgående billetter, seneste tweets og meget mere! Brugere kan tilføje widgets de kan lide og ændre baggrunden efter deres smag.",
"\"{title} icon\"" : "\"{title} ikon\"",
"Customize" : "Tilpas",
"Edit widgets" : "Redigér widgets",
"Get more widgets from the App Store" : "Få flere widgets fra App Store",

View file

@ -1,6 +1,8 @@
{ "translations": {
"Dashboard" : "Dashboard",
"Dashboard app" : "Dashboard app",
"Start your day informed\n\nThe Nextcloud Dashboard is your starting point of the day, giving you an overview of your upcoming appointments, urgent emails, chat messages, incoming tickets, latest tweets and much more! Users can add the widgets they like and change the background to their liking." : "Start din dag informeret\n\nNextcloud Dashboard er dit udgangspunkt på dagen og giver dig en oversigt over dine kommende aftaler, hastemails, chatbeskeder, indgående billetter, seneste tweets og meget mere! Brugere kan tilføje widgets de kan lide og ændre baggrunden efter deres smag.",
"\"{title} icon\"" : "\"{title} ikon\"",
"Customize" : "Tilpas",
"Edit widgets" : "Redigér widgets",
"Get more widgets from the App Store" : "Få flere widgets fra App Store",

28
apps/dashboard/l10n/vi.js Normal file
View file

@ -0,0 +1,28 @@
OC.L10N.register(
"dashboard",
{
"Dashboard" : "Tổng quan",
"Dashboard app" : "Ứng dụng Tổng quan",
"Start your day informed\n\nThe Nextcloud Dashboard is your starting point of the day, giving you an overview of your upcoming appointments, urgent emails, chat messages, incoming tickets, latest tweets and much more! Users can add the widgets they like and change the background to their liking." : "Bắt đầu ngày mới của bạn được thông báo\n\nBảng điều khiển Nextcloud là điểm khởi đầu trong ngày của bạn, cung cấp cho bạn cái nhìn tổng quan về các cuộc hẹn sắp tới, email khẩn cấp, tin nhắn trò chuyện, vé đến, tweet mới nhất và hơn thế nữa! Người dùng có thể thêm các widget mình thích và thay đổi nền theo ý thích.",
"\"{title} icon\"" : "\"{title} icon\"",
"Customize" : "Tuỳ chỉnh",
"Edit widgets" : "Chỉnh sửa widget",
"Get more widgets from the App Store" : "Tải thêm widget từ App Store",
"Weather service" : "Dịch vụ thời tiết",
"For your privacy, the weather data is requested by your Nextcloud server on your behalf so the weather service receives no personal information." : "Vì quyền riêng tư của bạn, dữ liệu thời tiết được yêu cầu bởi máy chủ Nextcloud thay mặt bạn để dịch vụ thời tiết không nhận được thông tin cá nhân.",
"Weather data from Met.no" : "Dữ liệu thời tiết từ Met.no",
"geocoding with Nominatim" : "mã hóa địa lý với Nominatim",
"elevation data from OpenTopoData" : "dữ liệu độ cao từ OpenTopoData",
"Weather" : "Thời tiết",
"Status" : "Trạng thái",
"Good morning" : "Chào buổi sáng",
"Good morning, {name}" : "Chào buổi sáng, {name}",
"Good afternoon" : "Chào buổi chiều",
"Good afternoon, {name}" : "Chào buổi chiều, {name}",
"Good evening" : "Chào buổi tối",
"Good evening, {name}" : "Chào buổi tối, {name}",
"Hello" : "Xin chào",
"Hello, {name}" : "Xin chào, {name}",
"Start your day informed\n\nThe Nextcloud Dashboard is your starting point of the day, giving you an\noverview of your upcoming appointments, urgent emails, chat messages,\nincoming tickets, latest tweets and much more! Users can add the widgets\nthey like and change the background to their liking." : "Bắt đầu ngày mới của bạn\n\nBảng điều khiển Nextcloud là điểm khởi đầu trong ngày của bạn, cung cấp cho bạn\ntổng quan về các cuộc hẹn sắp tới của bạn, email khẩn cấp, tin nhắn trò chuyện,\nvé đến, tweet mới nhất và nhiều hơn nữa! Người dùng có thể thêm các widget\nhọ thích và thay đổi nền theo ý thích của họ."
},
"nplurals=1; plural=0;");

View file

@ -0,0 +1,26 @@
{ "translations": {
"Dashboard" : "Tổng quan",
"Dashboard app" : "Ứng dụng Tổng quan",
"Start your day informed\n\nThe Nextcloud Dashboard is your starting point of the day, giving you an overview of your upcoming appointments, urgent emails, chat messages, incoming tickets, latest tweets and much more! Users can add the widgets they like and change the background to their liking." : "Bắt đầu ngày mới của bạn được thông báo\n\nBảng điều khiển Nextcloud là điểm khởi đầu trong ngày của bạn, cung cấp cho bạn cái nhìn tổng quan về các cuộc hẹn sắp tới, email khẩn cấp, tin nhắn trò chuyện, vé đến, tweet mới nhất và hơn thế nữa! Người dùng có thể thêm các widget mình thích và thay đổi nền theo ý thích.",
"\"{title} icon\"" : "\"{title} icon\"",
"Customize" : "Tuỳ chỉnh",
"Edit widgets" : "Chỉnh sửa widget",
"Get more widgets from the App Store" : "Tải thêm widget từ App Store",
"Weather service" : "Dịch vụ thời tiết",
"For your privacy, the weather data is requested by your Nextcloud server on your behalf so the weather service receives no personal information." : "Vì quyền riêng tư của bạn, dữ liệu thời tiết được yêu cầu bởi máy chủ Nextcloud thay mặt bạn để dịch vụ thời tiết không nhận được thông tin cá nhân.",
"Weather data from Met.no" : "Dữ liệu thời tiết từ Met.no",
"geocoding with Nominatim" : "mã hóa địa lý với Nominatim",
"elevation data from OpenTopoData" : "dữ liệu độ cao từ OpenTopoData",
"Weather" : "Thời tiết",
"Status" : "Trạng thái",
"Good morning" : "Chào buổi sáng",
"Good morning, {name}" : "Chào buổi sáng, {name}",
"Good afternoon" : "Chào buổi chiều",
"Good afternoon, {name}" : "Chào buổi chiều, {name}",
"Good evening" : "Chào buổi tối",
"Good evening, {name}" : "Chào buổi tối, {name}",
"Hello" : "Xin chào",
"Hello, {name}" : "Xin chào, {name}",
"Start your day informed\n\nThe Nextcloud Dashboard is your starting point of the day, giving you an\noverview of your upcoming appointments, urgent emails, chat messages,\nincoming tickets, latest tweets and much more! Users can add the widgets\nthey like and change the background to their liking." : "Bắt đầu ngày mới của bạn\n\nBảng điều khiển Nextcloud là điểm khởi đầu trong ngày của bạn, cung cấp cho bạn\ntổng quan về các cuộc hẹn sắp tới của bạn, email khẩn cấp, tin nhắn trò chuyện,\nvé đến, tweet mới nhất và nhiều hơn nữa! Người dùng có thể thêm các widget\nhọ thích và thay đổi nền theo ý thích của họ."
},"pluralForm" :"nplurals=1; plural=0;"
}

View file

@ -2,6 +2,7 @@ OC.L10N.register(
"dav",
{
"Calendar" : "Kalender",
"To-dos" : "Opgaver",
"Personal" : "Personligt",
"{actor} created calendar {calendar}" : "{actor} oprettede kalenderen {calendar}",
"You created calendar {calendar}" : "Du oprettede kalenderen {calendar}",
@ -31,12 +32,27 @@ OC.L10N.register(
"You deleted event {event} from calendar {calendar}" : "Du slettede begivenheden {event} fra kalenderen {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} opdaterede begivenheden {event} i kalenderen {calendar}",
"You updated event {event} in calendar {calendar}" : "Du opdaterede begivenheden {event} i kalenderen {calendar}",
"{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} flyttede begivenhed {event} fra kalender {sourceCalendar} til kalender {targetCalendar}",
"You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du flyttede begivenhed {event} fra kalender {sourceCalendar} til kalender {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} gendannede begivenhed {event} i kalender {calendar}",
"You restored event {event} of calendar {calendar}" : "Du gendannede begivenhed {begivenhed} i kalender {kalender}",
"Busy" : "Optaget",
"{actor} created to-do {todo} in list {calendar}" : "{actor} oprettede en opgave {todo} i listen {calendar}",
"You created to-do {todo} in list {calendar}" : "Du oprettede opgaven {todo} i listen {calendar}",
"{actor} deleted to-do {todo} from list {calendar}" : "{actor} slettede opgaven {todo} fra listen {calendar}",
"You deleted to-do {todo} from list {calendar}" : "Du slettede opgaven {todo} fra listen {calendar}",
"{actor} updated to-do {todo} in list {calendar}" : "{actor} opdaterede opgaven {todo} i listen {calendar}",
"You updated to-do {todo} in list {calendar}" : "Du opdaterede opgaven {todo} i listen {calendar}",
"{actor} solved to-do {todo} in list {calendar}" : "{actor} løste opgaven {todo} i listen {calendar}",
"You solved to-do {todo} in list {calendar}" : "Du løste opgaven {todo} i listen {calendar}",
"{actor} reopened to-do {todo} in list {calendar}" : "{actor} genåbnede opgaven {todo} i listen {calendar}",
"You reopened to-do {todo} in list {calendar}" : "Du genåbnede opgaven {todo} i listen {calendar}",
"{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} flyttede opgave {event} fra liste {sourceCalendar} til liste {targetCalendar}",
"You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du flyttede opgave {event} fra liste {sourceCalendar} til liste {targetCalendar}",
"Calendar, contacts and tasks" : "Kalender, kontakter og opgaver",
"A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> er blevet ændret",
"A calendar <strong>event</strong> was modified" : "En kalender <strong>begivenhed</strong> er blevet ændret",
"A calendar <strong>to-do</strong> was modified" : "En kalender <strong>opgave</strong> blev ændret",
"Contact birthdays" : "Kontakt fødselsdag",
"Death of %s" : "Død af%s",
"Untitled calendar" : "Unanvngiven kalender",
@ -56,6 +72,17 @@ OC.L10N.register(
"Description: %s" : "Beskrivelse: %s",
"Where: %s" : "Hvor: %s",
"%1$s via %2$s" : "%1$s via %2$s",
"Cancelled: %1$s" : "Annulléret: %1$s",
"\"%1$s\" has been canceled" : "\"%1$s\" er blevet annulleret",
"Re: %1$s" : "Re: %1$s",
"%1$s has accepted your invitation" : "%1$s har accepteret din invitation",
"%1$s has tentatively accepted your invitation" : "%1$s har foreløbigt accepteret din invitation",
"%1$s has declined your invitation" : "%1$s har afvist din invitation",
"%1$s has responded to your invitation" : "%1$s har svaret på din invitation",
"Invitation updated: %1$s" : "Invitation opdateret: %1$s",
"%1$s updated the event \"%2$s\"" : "%1$s opdaterede begivenheden \"%2$s\"",
"Invitation: %1$s" : "Invitation: %1$s",
"%1$s would like to invite you to \"%2$s\"" : "%1$s vil gerne invitere dig til \"%2$s\"",
"Organizer:" : "Arrangør:",
"Attendees:" : "Deltagere:",
"Title:" : "Titel:",
@ -65,17 +92,74 @@ OC.L10N.register(
"Accept" : "Accepter",
"Decline" : "Afvis",
"More options …" : "Flere indstillinger…",
"More options at %s" : "Flere muligheder på %s",
"Contacts" : "Kontakter",
"{actor} created address book {addressbook}" : "{actor} oprettede adressebog {addressbook}",
"You created address book {addressbook}" : "Du oprettede adressebog {addressbook}",
"{actor} deleted address book {addressbook}" : "{actor} slattede adressebog {addressbook}",
"You deleted address book {addressbook}" : "Du slettede adressebog {addressbook}",
"{actor} updated address book {addressbook}" : "{actor} opdaterede adressebog {addressbook}",
"You updated address book {addressbook}" : "Du opdaterede adressebog {addressbook}",
"{actor} shared address book {addressbook} with you" : "{actor} delte adressebog {addressbook} med dig",
"You shared address book {addressbook} with {user}" : "Du delte adressebog {addressbook} med {user}",
"{actor} shared address book {addressbook} with {user}" : "{actor} delte adressebog {addressbook} med {user}",
"{actor} unshared address book {addressbook} from you" : "{actor} fjernede delingen af adressebog {addressbook} med dig",
"You unshared address book {addressbook} from {user}" : "Du fjernede delingen af adressebog {addressbook} med {user}",
"{actor} unshared address book {addressbook} from {user}" : "{actor} fjernede delingen af adressebog {addressbook} med {user}",
"{actor} unshared address book {addressbook} from themselves" : "{actor} fjernede delingen af adressebog {addressbook} med sig selv",
"You shared address book {addressbook} with group {group}" : "Du delte adressebog {addressbook} med gruppen {group}",
"{actor} shared address book {addressbook} with group {group}" : "{actor} delte adressebog {addressbook} med gruppen {group}",
"You unshared address book {addressbook} from group {group}" : "Du fjernede delingen af adressebog {addressbook} fra gruppen {group}",
"{actor} unshared address book {addressbook} from group {group}" : "{actor} fjernede delingen af adressebog {addressbook} med gruppen {group}",
"{actor} created contact {card} in address book {addressbook}" : "{actor} oprettede kontakten {card} i adressebog {addressbook}",
"You created contact {card} in address book {addressbook}" : "Du oprettede kontakten {card} i adressebog {addressbook}",
"{actor} deleted contact {card} from address book {addressbook}" : "{actor} slettede kontakten {card} i adressebog {addressbook}",
"You deleted contact {card} from address book {addressbook}" : "Du slettede kontakten {card} i adressebog {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} opdaterede kontakten {card} i adressebog {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Du opdaterede kontakten {card} i adressebog {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressebog</strong> blev ændret",
"Accounts" : "Konti",
"System address book which holds all accounts" : "Systemets adressebog, som indeholder alle konti",
"File is not updatable: %1$s" : "Filen kan ikke updateres: %1$s",
"Could not write to final file, canceled by hook" : "Kunne ikke skrive til den endelige fil, annulleret af hook",
"Could not write file contents" : "Kunne ikke skrive filindhold",
"_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Fejl under kopiering af fil til placering (kopieret: %1$s, forventet filstørrelse: %2$s)",
"Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Forventede filstørrelse på %1$s, men læste (fra Nextcloud-klienten) og skrev (til Nextcloud-lageret) %2$s. Det kan enten være et netværksproblem på afsendersiden eller et problem med at skrive til lageret på serversiden.",
"Could not rename part file to final file, canceled by hook" : "Kunne ikke omdøbe delfilen til den endelige fil, annulleret af hook",
"Could not rename part file to final file" : "Delfilen kunne ikke omdøbes til den endelige fil",
"Failed to check file size: %1$s" : "Kunne ikke kontrollere filstørrelsen: %1$s",
"Could not open file" : "Kunne ikke åbne fil",
"Encryption not ready: %1$s" : "Kryptering ikke klar: %1$s",
"Failed to open file: %1$s" : "Kunne ikke åbne fil: %1$s",
"Failed to unlink: %1$s" : "Tilknytningen kunne ikke fjernes: %1$s",
"Invalid chunk name" : "Ugyldigt stykke navn",
"Could not rename part file assembled from chunks" : "Kunne ikke omdøbe delfilen samlet fra stykker",
"Failed to write file contents: %1$s" : "Kunne ikke skrive filindhold: %1$s",
"File not found: %1$s" : "Fil ikke fundet: %1$s",
"System is in maintenance mode." : "Systemet er i vedligeholdelsestilstand.",
"Upgrade needed" : "Opgradering er nødvendig",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Din %s skal konfigureres til at bruge HTTPS for at kunne bruge CalDAV og CardDAV med iOS/macOS.",
"Configures a CalDAV account" : "Konfigurerer en CalDAV-konto",
"Configures a CardDAV account" : "Konfigurerer en CardDAV-konto",
"Events" : "Begivenheder",
"Tasks" : "Opgaver",
"Untitled task" : "Unavngivet opgave",
"Completed on %s" : "Fuldført den %s",
"Due on %s by %s" : "Forfalder på %s til %s",
"Due on %s" : "Forfalder på %s",
"Migrated calendar (%1$s)" : "Migreret kalender (%1$s)",
"Calendars including events, details and attendees" : "Kalendere indeholdende begivenheder, detaljer og deltagere",
"Contacts and groups" : "Kontakter og grupper",
"WebDAV" : "WebDAV",
"WebDAV endpoint" : "WebDAV endpoint",
"Availability" : "tilgængelighed",
"If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Hvis du konfigurerer din arbejdstid, vil andre brugere se, når du er fraværende, når de booker et møde.",
"Time zone:" : "Tidszone:",
"to" : "til",
"Delete slot" : "Slet slot",
"No working hours set" : "Arbejdstider er ikke sat",
"Add slot" : "Tilføj slot",
"Monday" : "Mandag",
"Tuesday" : "Tirsdag",
"Wednesday" : "Onsdag",
@ -83,28 +167,28 @@ OC.L10N.register(
"Friday" : "Fredag",
"Saturday" : "Lørdag",
"Sunday" : "Søndag",
"Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Indstil automatisk brugerstatus til \"Forstyr ikke\" uden for tilgængelighed for at slå alle notifikationer fra.",
"Save" : "Gem",
"Failed to load availability" : "Kunne ikke indlæse tilgængelighed",
"Saved availability" : "Gemt tilgængelighed",
"Failed to save availability" : "Kunne ikke gemme tilgængelighed",
"Calendar server" : "Kalenderserver",
"Send invitations to attendees" : "Send invitation til deltagere",
"Automatically generate a birthday calendar" : "Generer en fødselsdagskalender automatisk",
"Birthday calendars will be generated by a background job." : "Fødselsdagskalendere vil blive oprettet af et job, der kører i baggrunden.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Derfor vil de ikke blive synlige med det samme efter aktivering, men vil vise sig efter noget tid.",
"Send notifications for events" : "Send meddelelser om begivenheder",
"Notifications are sent via background jobs, so these must occur often enough." : "Underretninger sendes via baggrundsjob, så disse skal ske ofte nok.",
"Send reminder notifications to calendar sharees as well" : "Send også påmindelsesmeddelelser til kalenderdelinger",
"Reminders are always sent to organizers and attendees." : "Påmindelser sendes altid til arrangører og deltagere.",
"Enable notifications for events via push" : "Aktiver notifikationer for begivenheder via push",
"Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer også {calendarappstoreopen}Kalender-appen{linkclose}, eller {calendardocopen}tilslut dit skrivebord og din mobil til synkronisering ↗{linkclose}.",
"Please make sure to properly set up {emailopen}the email server{linkclose}." : "Sørg for at konfigurere {emailopen}e-mail-serveren{linkclose} korrekt.",
"There was an error updating your attendance status." : "Der opstod en fejl under opdatering af din fremmødestatus.",
"Please contact the organizer directly." : "Kontakt venligst arrangøren direkte.",
"Are you accepting the invitation?" : "Accepter du invitationen?",
"Tentative" : "Foreløbig",
"Todos" : "Opgaver",
"{actor} created todo {todo} in list {calendar}" : "{actor} oprettede en opgave {todo} i listen {calendar}",
"You created todo {todo} in list {calendar}" : "Du oprettede opgaven {todo} i listen {calendar}",
"{actor} deleted todo {todo} from list {calendar}" : "{actor} slettede opgaven {todo} fra listen {calendar}",
"You deleted todo {todo} from list {calendar}" : "Du slettede opgaven {todo} fra listen {calendar}",
"{actor} updated todo {todo} in list {calendar}" : "{actor} opdaterede opgaven {todo} i listen {calendar}",
"You updated todo {todo} in list {calendar}" : "Du opdaterede opgaven {todo} i listen {calendar}",
"{actor} solved todo {todo} in list {calendar}" : "{actor} løste opgaven {todo} i listen {calendar}",
"You solved todo {todo} in list {calendar}" : "Du løste opgaven {todo} i listen {calendar}",
"{actor} reopened todo {todo} in list {calendar}" : "{actor} genåbnede opgaven {todo} i listen {calendar}",
"You reopened todo {todo} in list {calendar}" : "Du genåbnede opgaven {todo} i listen {calendar}",
"A calendar <strong>todo</strong> was modified" : "En kalender <strong>opgave</strong> blev ændret",
"Invitation canceled" : "Invitation annulleret",
"Invitation updated" : "Invitation opdateret ",
"Invitation" : "Invitation"
"Your attendance was updated successfully." : "Dit tilstedeværelse blev opdateret.",
"%1$s has responded your invitation" : "%1$s har besvaret din invitation"
},
"nplurals=2; plural=(n != 1);");

View file

@ -1,5 +1,6 @@
{ "translations": {
"Calendar" : "Kalender",
"To-dos" : "Opgaver",
"Personal" : "Personligt",
"{actor} created calendar {calendar}" : "{actor} oprettede kalenderen {calendar}",
"You created calendar {calendar}" : "Du oprettede kalenderen {calendar}",
@ -29,12 +30,27 @@
"You deleted event {event} from calendar {calendar}" : "Du slettede begivenheden {event} fra kalenderen {calendar}",
"{actor} updated event {event} in calendar {calendar}" : "{actor} opdaterede begivenheden {event} i kalenderen {calendar}",
"You updated event {event} in calendar {calendar}" : "Du opdaterede begivenheden {event} i kalenderen {calendar}",
"{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "{actor} flyttede begivenhed {event} fra kalender {sourceCalendar} til kalender {targetCalendar}",
"You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}" : "Du flyttede begivenhed {event} fra kalender {sourceCalendar} til kalender {targetCalendar}",
"{actor} restored event {event} of calendar {calendar}" : "{actor} gendannede begivenhed {event} i kalender {calendar}",
"You restored event {event} of calendar {calendar}" : "Du gendannede begivenhed {begivenhed} i kalender {kalender}",
"Busy" : "Optaget",
"{actor} created to-do {todo} in list {calendar}" : "{actor} oprettede en opgave {todo} i listen {calendar}",
"You created to-do {todo} in list {calendar}" : "Du oprettede opgaven {todo} i listen {calendar}",
"{actor} deleted to-do {todo} from list {calendar}" : "{actor} slettede opgaven {todo} fra listen {calendar}",
"You deleted to-do {todo} from list {calendar}" : "Du slettede opgaven {todo} fra listen {calendar}",
"{actor} updated to-do {todo} in list {calendar}" : "{actor} opdaterede opgaven {todo} i listen {calendar}",
"You updated to-do {todo} in list {calendar}" : "Du opdaterede opgaven {todo} i listen {calendar}",
"{actor} solved to-do {todo} in list {calendar}" : "{actor} løste opgaven {todo} i listen {calendar}",
"You solved to-do {todo} in list {calendar}" : "Du løste opgaven {todo} i listen {calendar}",
"{actor} reopened to-do {todo} in list {calendar}" : "{actor} genåbnede opgaven {todo} i listen {calendar}",
"You reopened to-do {todo} in list {calendar}" : "Du genåbnede opgaven {todo} i listen {calendar}",
"{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "{actor} flyttede opgave {event} fra liste {sourceCalendar} til liste {targetCalendar}",
"You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}" : "Du flyttede opgave {event} fra liste {sourceCalendar} til liste {targetCalendar}",
"Calendar, contacts and tasks" : "Kalender, kontakter og opgaver",
"A <strong>calendar</strong> was modified" : "En <strong>kalender</strong> er blevet ændret",
"A calendar <strong>event</strong> was modified" : "En kalender <strong>begivenhed</strong> er blevet ændret",
"A calendar <strong>to-do</strong> was modified" : "En kalender <strong>opgave</strong> blev ændret",
"Contact birthdays" : "Kontakt fødselsdag",
"Death of %s" : "Død af%s",
"Untitled calendar" : "Unanvngiven kalender",
@ -54,6 +70,17 @@
"Description: %s" : "Beskrivelse: %s",
"Where: %s" : "Hvor: %s",
"%1$s via %2$s" : "%1$s via %2$s",
"Cancelled: %1$s" : "Annulléret: %1$s",
"\"%1$s\" has been canceled" : "\"%1$s\" er blevet annulleret",
"Re: %1$s" : "Re: %1$s",
"%1$s has accepted your invitation" : "%1$s har accepteret din invitation",
"%1$s has tentatively accepted your invitation" : "%1$s har foreløbigt accepteret din invitation",
"%1$s has declined your invitation" : "%1$s har afvist din invitation",
"%1$s has responded to your invitation" : "%1$s har svaret på din invitation",
"Invitation updated: %1$s" : "Invitation opdateret: %1$s",
"%1$s updated the event \"%2$s\"" : "%1$s opdaterede begivenheden \"%2$s\"",
"Invitation: %1$s" : "Invitation: %1$s",
"%1$s would like to invite you to \"%2$s\"" : "%1$s vil gerne invitere dig til \"%2$s\"",
"Organizer:" : "Arrangør:",
"Attendees:" : "Deltagere:",
"Title:" : "Titel:",
@ -63,17 +90,74 @@
"Accept" : "Accepter",
"Decline" : "Afvis",
"More options …" : "Flere indstillinger…",
"More options at %s" : "Flere muligheder på %s",
"Contacts" : "Kontakter",
"{actor} created address book {addressbook}" : "{actor} oprettede adressebog {addressbook}",
"You created address book {addressbook}" : "Du oprettede adressebog {addressbook}",
"{actor} deleted address book {addressbook}" : "{actor} slattede adressebog {addressbook}",
"You deleted address book {addressbook}" : "Du slettede adressebog {addressbook}",
"{actor} updated address book {addressbook}" : "{actor} opdaterede adressebog {addressbook}",
"You updated address book {addressbook}" : "Du opdaterede adressebog {addressbook}",
"{actor} shared address book {addressbook} with you" : "{actor} delte adressebog {addressbook} med dig",
"You shared address book {addressbook} with {user}" : "Du delte adressebog {addressbook} med {user}",
"{actor} shared address book {addressbook} with {user}" : "{actor} delte adressebog {addressbook} med {user}",
"{actor} unshared address book {addressbook} from you" : "{actor} fjernede delingen af adressebog {addressbook} med dig",
"You unshared address book {addressbook} from {user}" : "Du fjernede delingen af adressebog {addressbook} med {user}",
"{actor} unshared address book {addressbook} from {user}" : "{actor} fjernede delingen af adressebog {addressbook} med {user}",
"{actor} unshared address book {addressbook} from themselves" : "{actor} fjernede delingen af adressebog {addressbook} med sig selv",
"You shared address book {addressbook} with group {group}" : "Du delte adressebog {addressbook} med gruppen {group}",
"{actor} shared address book {addressbook} with group {group}" : "{actor} delte adressebog {addressbook} med gruppen {group}",
"You unshared address book {addressbook} from group {group}" : "Du fjernede delingen af adressebog {addressbook} fra gruppen {group}",
"{actor} unshared address book {addressbook} from group {group}" : "{actor} fjernede delingen af adressebog {addressbook} med gruppen {group}",
"{actor} created contact {card} in address book {addressbook}" : "{actor} oprettede kontakten {card} i adressebog {addressbook}",
"You created contact {card} in address book {addressbook}" : "Du oprettede kontakten {card} i adressebog {addressbook}",
"{actor} deleted contact {card} from address book {addressbook}" : "{actor} slettede kontakten {card} i adressebog {addressbook}",
"You deleted contact {card} from address book {addressbook}" : "Du slettede kontakten {card} i adressebog {addressbook}",
"{actor} updated contact {card} in address book {addressbook}" : "{actor} opdaterede kontakten {card} i adressebog {addressbook}",
"You updated contact {card} in address book {addressbook}" : "Du opdaterede kontakten {card} i adressebog {addressbook}",
"A <strong>contact</strong> or <strong>address book</strong> was modified" : "En <strong>kontakt</strong> eller <strong>adressebog</strong> blev ændret",
"Accounts" : "Konti",
"System address book which holds all accounts" : "Systemets adressebog, som indeholder alle konti",
"File is not updatable: %1$s" : "Filen kan ikke updateres: %1$s",
"Could not write to final file, canceled by hook" : "Kunne ikke skrive til den endelige fil, annulleret af hook",
"Could not write file contents" : "Kunne ikke skrive filindhold",
"_%n byte_::_%n bytes_" : ["%n byte","%n bytes"],
"Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Fejl under kopiering af fil til placering (kopieret: %1$s, forventet filstørrelse: %2$s)",
"Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Forventede filstørrelse på %1$s, men læste (fra Nextcloud-klienten) og skrev (til Nextcloud-lageret) %2$s. Det kan enten være et netværksproblem på afsendersiden eller et problem med at skrive til lageret på serversiden.",
"Could not rename part file to final file, canceled by hook" : "Kunne ikke omdøbe delfilen til den endelige fil, annulleret af hook",
"Could not rename part file to final file" : "Delfilen kunne ikke omdøbes til den endelige fil",
"Failed to check file size: %1$s" : "Kunne ikke kontrollere filstørrelsen: %1$s",
"Could not open file" : "Kunne ikke åbne fil",
"Encryption not ready: %1$s" : "Kryptering ikke klar: %1$s",
"Failed to open file: %1$s" : "Kunne ikke åbne fil: %1$s",
"Failed to unlink: %1$s" : "Tilknytningen kunne ikke fjernes: %1$s",
"Invalid chunk name" : "Ugyldigt stykke navn",
"Could not rename part file assembled from chunks" : "Kunne ikke omdøbe delfilen samlet fra stykker",
"Failed to write file contents: %1$s" : "Kunne ikke skrive filindhold: %1$s",
"File not found: %1$s" : "Fil ikke fundet: %1$s",
"System is in maintenance mode." : "Systemet er i vedligeholdelsestilstand.",
"Upgrade needed" : "Opgradering er nødvendig",
"Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Din %s skal konfigureres til at bruge HTTPS for at kunne bruge CalDAV og CardDAV med iOS/macOS.",
"Configures a CalDAV account" : "Konfigurerer en CalDAV-konto",
"Configures a CardDAV account" : "Konfigurerer en CardDAV-konto",
"Events" : "Begivenheder",
"Tasks" : "Opgaver",
"Untitled task" : "Unavngivet opgave",
"Completed on %s" : "Fuldført den %s",
"Due on %s by %s" : "Forfalder på %s til %s",
"Due on %s" : "Forfalder på %s",
"Migrated calendar (%1$s)" : "Migreret kalender (%1$s)",
"Calendars including events, details and attendees" : "Kalendere indeholdende begivenheder, detaljer og deltagere",
"Contacts and groups" : "Kontakter og grupper",
"WebDAV" : "WebDAV",
"WebDAV endpoint" : "WebDAV endpoint",
"Availability" : "tilgængelighed",
"If you configure your working hours, other users will see when you are out of office when they book a meeting." : "Hvis du konfigurerer din arbejdstid, vil andre brugere se, når du er fraværende, når de booker et møde.",
"Time zone:" : "Tidszone:",
"to" : "til",
"Delete slot" : "Slet slot",
"No working hours set" : "Arbejdstider er ikke sat",
"Add slot" : "Tilføj slot",
"Monday" : "Mandag",
"Tuesday" : "Tirsdag",
"Wednesday" : "Onsdag",
@ -81,28 +165,28 @@
"Friday" : "Fredag",
"Saturday" : "Lørdag",
"Sunday" : "Søndag",
"Automatically set user status to \"Do not disturb\" outside of availability to mute all notifications." : "Indstil automatisk brugerstatus til \"Forstyr ikke\" uden for tilgængelighed for at slå alle notifikationer fra.",
"Save" : "Gem",
"Failed to load availability" : "Kunne ikke indlæse tilgængelighed",
"Saved availability" : "Gemt tilgængelighed",
"Failed to save availability" : "Kunne ikke gemme tilgængelighed",
"Calendar server" : "Kalenderserver",
"Send invitations to attendees" : "Send invitation til deltagere",
"Automatically generate a birthday calendar" : "Generer en fødselsdagskalender automatisk",
"Birthday calendars will be generated by a background job." : "Fødselsdagskalendere vil blive oprettet af et job, der kører i baggrunden.",
"Hence they will not be available immediately after enabling but will show up after some time." : "Derfor vil de ikke blive synlige med det samme efter aktivering, men vil vise sig efter noget tid.",
"Send notifications for events" : "Send meddelelser om begivenheder",
"Notifications are sent via background jobs, so these must occur often enough." : "Underretninger sendes via baggrundsjob, så disse skal ske ofte nok.",
"Send reminder notifications to calendar sharees as well" : "Send også påmindelsesmeddelelser til kalenderdelinger",
"Reminders are always sent to organizers and attendees." : "Påmindelser sendes altid til arrangører og deltagere.",
"Enable notifications for events via push" : "Aktiver notifikationer for begivenheder via push",
"Also install the {calendarappstoreopen}Calendar app{linkclose}, or {calendardocopen}connect your desktop & mobile for syncing ↗{linkclose}." : "Installer også {calendarappstoreopen}Kalender-appen{linkclose}, eller {calendardocopen}tilslut dit skrivebord og din mobil til synkronisering ↗{linkclose}.",
"Please make sure to properly set up {emailopen}the email server{linkclose}." : "Sørg for at konfigurere {emailopen}e-mail-serveren{linkclose} korrekt.",
"There was an error updating your attendance status." : "Der opstod en fejl under opdatering af din fremmødestatus.",
"Please contact the organizer directly." : "Kontakt venligst arrangøren direkte.",
"Are you accepting the invitation?" : "Accepter du invitationen?",
"Tentative" : "Foreløbig",
"Todos" : "Opgaver",
"{actor} created todo {todo} in list {calendar}" : "{actor} oprettede en opgave {todo} i listen {calendar}",
"You created todo {todo} in list {calendar}" : "Du oprettede opgaven {todo} i listen {calendar}",
"{actor} deleted todo {todo} from list {calendar}" : "{actor} slettede opgaven {todo} fra listen {calendar}",
"You deleted todo {todo} from list {calendar}" : "Du slettede opgaven {todo} fra listen {calendar}",
"{actor} updated todo {todo} in list {calendar}" : "{actor} opdaterede opgaven {todo} i listen {calendar}",
"You updated todo {todo} in list {calendar}" : "Du opdaterede opgaven {todo} i listen {calendar}",
"{actor} solved todo {todo} in list {calendar}" : "{actor} løste opgaven {todo} i listen {calendar}",
"You solved todo {todo} in list {calendar}" : "Du løste opgaven {todo} i listen {calendar}",
"{actor} reopened todo {todo} in list {calendar}" : "{actor} genåbnede opgaven {todo} i listen {calendar}",
"You reopened todo {todo} in list {calendar}" : "Du genåbnede opgaven {todo} i listen {calendar}",
"A calendar <strong>todo</strong> was modified" : "En kalender <strong>opgave</strong> blev ændret",
"Invitation canceled" : "Invitation annulleret",
"Invitation updated" : "Invitation opdateret ",
"Invitation" : "Invitation"
"Your attendance was updated successfully." : "Dit tilstedeværelse blev opdateret.",
"%1$s has responded your invitation" : "%1$s har besvaret din invitation"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
}

View file

@ -1845,9 +1845,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->andWhere($innerQuery->expr()->eq('op.calendartype',
$outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
->from('calendarobjects', 'c')
->where($outerQuery->expr()->isNull('deleted_at'));
// only return public items for shared calendars for now
if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
$innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
$outerQuery->andWhere($outerQuery->expr()->eq('c.classification',
$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
}
@ -1866,10 +1870,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->db->escapeLikeParameter($pattern) . '%')));
}
$outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
->from('calendarobjects', 'c')
->where($outerQuery->expr()->isNull('deleted_at'));
if (isset($options['timerange'])) {
if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
$outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',

View file

@ -6,6 +6,7 @@ declare(strict_types=1);
* @copyright 2022 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
@ -105,6 +106,40 @@ class IMipService {
return $newstring;
}
/**
* Like generateDiffString() but linkifies the property values if they are urls.
*/
private function generateLinkifiedDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
if (!isset($vevent->$property)) {
return $default;
}
/** @var string|null $newString */
$newString = $vevent->$property->getValue();
$oldString = isset($oldVEvent->$property) ? $oldVEvent->$property->getValue() : null;
if ($oldString !== $newString) {
return sprintf(
"<span style='text-decoration: line-through'>%s</span><br />%s",
$this->linkify($oldString) ?? $oldString ?? '',
$this->linkify($newString) ?? $newString ?? ''
);
}
return $this->linkify($newString) ?? $newString;
}
/**
* Convert a given url to a html link element or return null otherwise.
*/
private function linkify(?string $url): ?string {
if ($url === null) {
return null;
}
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
return null;
}
return sprintf('<a href="%1$s">%1$s</a>', htmlspecialchars($url));
}
/**
* @param VEvent $vEvent
* @param VEvent|null $oldVEvent
@ -121,11 +156,15 @@ class IMipService {
$data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal);
if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
$data['meeting_location_html'] = $locationHtml;
}
if(!empty($oldVEvent)) {
$oldMeetingWhen = $this->generateWhenString($oldVEvent);
$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
$data['meeting_location_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
$data['meeting_location_html'] = $this->generateLinkifiedDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
$data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
@ -246,6 +285,7 @@ class IMipService {
$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
$newLocationHtml = $this->linkify($newLocation) ?? $newLocation;
$data = [];
$data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen);
@ -256,7 +296,7 @@ class IMipService {
$data['meeting_description'] = $newDescription;
$data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : '';
$data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : '';
$data['meeting_location_html'] = $newLocation !== '' ? sprintf($strikethrough, $newLocation) : '';
$data['meeting_location_html'] = $newLocationHtml !== '' ? sprintf($strikethrough, $newLocationHtml) : '';
$data['meeting_location'] = $newLocation;
return $data;
}

View file

@ -46,6 +46,7 @@ use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IFile;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
use Sabre\DAV\ServerPlugin;
@ -84,6 +85,16 @@ class FilesPlugin extends ServerPlugin {
public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count';
public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count';
public const FILE_METADATA_SIZE = '{http://nextcloud.org/ns}file-metadata-size';
public const FILE_METADATA_GPS = '{http://nextcloud.org/ns}file-metadata-gps';
public const ALL_METADATA_PROPS = [
self::FILE_METADATA_SIZE => 'size',
self::FILE_METADATA_GPS => 'gps',
];
public const METADATA_MIMETYPES = [
'size' => 'image',
'gps' => 'image',
];
/** Reference to main server object */
private ?Server $server = null;
@ -418,26 +429,28 @@ class FilesPlugin extends ServerPlugin {
});
if ($this->config->getSystemValueBool('enable_file_metadata', true)) {
$propFind->handle(self::FILE_METADATA_SIZE, function () use ($node) {
if (!str_starts_with($node->getFileInfo()->getMimetype(), 'image')) {
return json_encode((object)[], JSON_THROW_ON_ERROR);
}
foreach (self::ALL_METADATA_PROPS as $prop => $meta) {
$propFind->handle($prop, function () use ($node, $meta) {
if ($node->getFileInfo()->getMimePart() !== self::METADATA_MIMETYPES[$meta]) {
return [];
}
if ($node->hasMetadata('size')) {
$sizeMetadata = $node->getMetadata('size');
} else {
// This code path should not be called since we try to preload
// the metadata when loading the folder or the search results
// in one go
$metadataManager = \OC::$server->get(IMetadataManager::class);
$sizeMetadata = $metadataManager->fetchMetadataFor('size', [$node->getId()])[$node->getId()];
if ($node->hasMetadata($meta)) {
$metadata = $node->getMetadata($meta);
} else {
// This code path should not be called since we try to preload
// the metadata when loading the folder or the search results
// in one go
$metadataManager = \OC::$server->get(IMetadataManager::class);
$metadata = $metadataManager->fetchMetadataFor($meta, [$node->getId()])[$node->getId()];
// TODO would be nice to display this in the profiler...
\OC::$server->get(LoggerInterface::class)->debug('Inefficient fetching of metadata');
}
// TODO would be nice to display this in the profiler...
\OC::$server->get(LoggerInterface::class)->debug('Inefficient fetching of metadata');
}
return $sizeMetadata->getValue();
});
return $metadata->getValue();
});
}
}
}
@ -452,27 +465,31 @@ class FilesPlugin extends ServerPlugin {
$requestProperties = $propFind->getRequestedProperties();
// TODO detect dynamically which metadata groups are requested and
// preload all of them and not just size
if ($this->config->getSystemValueBool('enable_file_metadata', true)
&& in_array(self::FILE_METADATA_SIZE, $requestProperties, true)) {
// Preloading of the metadata
$fileIds = [];
foreach ($node->getChildren() as $child) {
/** @var \OCP\Files\Node|Node $child */
if (str_starts_with($child->getFileInfo()->getMimeType(), 'image/')) {
/** @var File $child */
$fileIds[] = $child->getFileInfo()->getId();
if ($this->config->getSystemValueBool('enable_file_metadata', true)) {
$requestedMetaData = [];
foreach ($requestProperties as $requestProperty) {
if (isset(self::ALL_METADATA_PROPS[$requestProperty])) {
$requestedMetaData[] = self::ALL_METADATA_PROPS[$requestProperty];
}
}
$children = $node->getChildren();
// Preloading of the metadata
/** @var IMetaDataManager $metadataManager */
$metadataManager = \OC::$server->get(IMetadataManager::class);
$preloadedMetadata = $metadataManager->fetchMetadataFor('size', $fileIds);
foreach ($node->getChildren() as $child) {
/** @var \OCP\Files\Node|Node $child */
if (str_starts_with($child->getFileInfo()->getMimeType(), 'image')) {
/** @var File $child */
$child->setMetadata('size', $preloadedMetadata[$child->getFileInfo()->getId()]);
foreach ($requestedMetaData as $requestedMeta) {
$relevantMimeType = self::METADATA_MIMETYPES[$requestedMeta];
$childrenForMeta = array_filter($children, function (INode $child) use ($relevantMimeType) {
return $child instanceof File && $child->getFileInfo()->getMimePart() === $relevantMimeType;
});
$fileIds = array_map(function (File $child) {
return $child->getFileInfo()->getId();
}, $childrenForMeta);
$preloadedMetadata = $metadataManager->fetchMetadataFor($requestedMeta, $fileIds);
foreach ($childrenForMeta as $child) {
$child->setMetadata($requestedMeta, $preloadedMetadata[$child->getFileInfo()->getId()]);
}
}
}

View file

@ -35,15 +35,20 @@ OC.L10N.register(
"Allow users on this server to receive group shares from other servers" : "Tillad brugere på denne server at modtage gruppedelinger fra andre servere",
"Search global and public address book for users" : "Søg global og offentlig adresse bog for brugere",
"Allow users to publish their data to a global and public address book" : "Tillad brugere at offentliggøre deres data til en global adressebog ",
"Unable to update federated files sharing config" : "Kan ikke opdatere fødereret fildelingskonfiguration",
"Federated Cloud" : "Federated Cloud",
"You can share with anyone who uses a Nextcloud server or other Open Cloud Mesh (OCM) compatible servers and services! Just put their Federated Cloud ID in the share dialog. It looks like person@cloud.example.com" : "Du kan dele med alle, der bruger en Nextcloud-server eller andre Open Cloud Mesh (OCM)-kompatible servere og tjenester! Indsæt blot deres Federated Cloud ID i delingsdialogen. Det ligner person@cloud.example.com",
"Your Federated Cloud ID:" : "Din Federated Cloud ID:",
"Share it so your friends can share files with you:" : "Del så dine venner kan dele filer med dig:",
"Facebook" : "Facebook",
"Twitter" : "Twitter",
"Diaspora" : "Diaspora",
"Add to your website" : "Tilføj til dit websted",
"Share with me via Nextcloud" : "Del med mig gennem Nextcloud",
"HTML Code:" : "HTMLkode:",
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Del med mig gennem min #Nextcloud Federated Cloud ID, se {url]",
"Share with me through my #Nextcloud Federated Cloud ID" : "Del med mig gennem min #Nextcloud Federated Cloud ID",
"Cloud ID copied to the clipboard" : "Cloud ID er kopieret til udklipsholderen.",
"Copy to clipboard" : "Kopier til udklipsholder",
"Clipboard is not available" : "Udklipsholderen er ikke tilgængelig",
"Copied!" : "Kopieret!",

View file

@ -33,15 +33,20 @@
"Allow users on this server to receive group shares from other servers" : "Tillad brugere på denne server at modtage gruppedelinger fra andre servere",
"Search global and public address book for users" : "Søg global og offentlig adresse bog for brugere",
"Allow users to publish their data to a global and public address book" : "Tillad brugere at offentliggøre deres data til en global adressebog ",
"Unable to update federated files sharing config" : "Kan ikke opdatere fødereret fildelingskonfiguration",
"Federated Cloud" : "Federated Cloud",
"You can share with anyone who uses a Nextcloud server or other Open Cloud Mesh (OCM) compatible servers and services! Just put their Federated Cloud ID in the share dialog. It looks like person@cloud.example.com" : "Du kan dele med alle, der bruger en Nextcloud-server eller andre Open Cloud Mesh (OCM)-kompatible servere og tjenester! Indsæt blot deres Federated Cloud ID i delingsdialogen. Det ligner person@cloud.example.com",
"Your Federated Cloud ID:" : "Din Federated Cloud ID:",
"Share it so your friends can share files with you:" : "Del så dine venner kan dele filer med dig:",
"Facebook" : "Facebook",
"Twitter" : "Twitter",
"Diaspora" : "Diaspora",
"Add to your website" : "Tilføj til dit websted",
"Share with me via Nextcloud" : "Del med mig gennem Nextcloud",
"HTML Code:" : "HTMLkode:",
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Del med mig gennem min #Nextcloud Federated Cloud ID, se {url]",
"Share with me through my #Nextcloud Federated Cloud ID" : "Del med mig gennem min #Nextcloud Federated Cloud ID",
"Cloud ID copied to the clipboard" : "Cloud ID er kopieret til udklipsholderen.",
"Copy to clipboard" : "Kopier til udklipsholder",
"Clipboard is not available" : "Udklipsholderen er ikke tilgængelig",
"Copied!" : "Kopieret!",

View file

@ -8,26 +8,52 @@ OC.L10N.register(
"Add remote share" : "افزودن هم‌رسانی دوردست",
"Invalid Federated Cloud ID" : "شناسهٔ ابری خودگردان نامعتبر",
"Server to server sharing is not enabled on this server" : "هم‌رسانی کارساز به کارساز روی این کارساز به کار نیفتاده",
"Couldn't establish a federated share." : "نمی توان یک سهم فدرال ایجاد کرد.",
"Couldn't establish a federated share, maybe the password was wrong." : "نمی‌توان یک اشتراک فدرال ایجاد کرد، شاید رمز عبور اشتباه بوده است.",
"Federated Share request sent, you will receive an invitation. Check your notifications." : "درخواست اشتراک فدرال ارسال شد، یک دعوت نامه دریافت خواهید کرد. اعلان های خود را بررسی کنید.",
"Couldn't establish a federated share, it looks like the server to federate with is too old (Nextcloud <= 9)." : "نمی‌توان یک اشتراک فدرال ایجاد کرد، به نظر می‌رسد سروری که باید با آن فدرال شود خیلی قدیمی است (Nextcloud <= 9).",
"It is not allowed to send federated group shares from this server." : "ارسال اشتراک های گروه فدرال از این سرور مجاز نیست.",
"Sharing %1$s failed, because this item is already shared with user %2$s" : "هم‌رسانی %1$s شکست خورد، چرا که این مورد از پیش با کاربر%2$s هم رسانی شده بود",
"Not allowed to create a federated share with the same user" : "ایجاد یک هم‌رسانی خودگران با همان کاربر مجاز نیست",
"Federated shares require read permissions" : "سهام فدرال به مجوز خواندن نیاز دارد",
"File is already shared with %s" : "فایل قبلاً با به اشتراک گذاشته شده است%s",
"Sharing %1$s failed, could not find %2$s, maybe the server is currently unreachable or uses a self-signed certificate." : "اشتراک‌گذاری %1$s انجام نشد، پیدا نشد%2$s، شاید سرور در حال حاضر غیرقابل دسترسی باشد یا از گواهی امضا شده استفاده می‌کند.",
"Could not find share" : "نتوانست هم‌رسانی را بیابد",
"Federated sharing" : "هم‌رسانی خودگردان",
"You received {share} as a remote share from {user} (on behalf of {behalf})" : "شما {share} را به عنوان یک اشتراک راه دور از {user} دریافت کردید (از طرف {behalf})",
"You received {share} as a remote share from {user}" : "شما {share} را به عنوان اشتراک راه دور از {user} دریافت کردید",
"Accept" : "قبول",
"Decline" : "کاهش می یابد",
"Federated Cloud Sharing" : "هم‌رسانی ابری خودگردان",
"Sharing" : "هم‌رسانی",
"Federated file sharing" : "هم‌رسانی پروندهٔ خودگردان",
"Provide federated file sharing across servers" : "فراهم‌کنندهٔ هم‌رسانی پروندهٔ خودگردان میان کارسازها",
"Adjust how people can share between servers. This includes shares between users on this server as well if they are using federated sharing." : "نحوه اشتراک گذاری افراد بین سرورها را تنظیم کنید. این شامل اشتراک گذاری بین کاربران در این سرور نیز می شود، در صورتی که از اشتراک گذاری فدرال استفاده می کنند.",
"Allow users on this server to send shares to other servers (this option also allows WebDAV access to public shares)" : "به کاربران این سرور اجازه می‌دهد تا اشتراک‌ها را به سرورهای دیگر ارسال کنند (این گزینه همچنین به WebDAV اجازه دسترسی به اشتراک‌های عمومی را می‌دهد)",
"Allow users on this server to receive shares from other servers" : "به کاربران موجود در این سرور اجازه دهید از سرورهای دیگر اشتراک‌گذاری دریافت کنند",
"Allow users on this server to send shares to groups on other servers" : "به کاربران موجود در این سرور امکان ارسال اشتراک‌ها به گروه‌های موجود در سرورهای دیگر را بدهید",
"Allow users on this server to receive group shares from other servers" : "به کاربران این سرور اجازه دهید اشتراک‌های گروهی را از سرورهای دیگر دریافت کنند",
"Search global and public address book for users" : "کتاب آدرس عمومی و عمومی را برای کاربران جستجو کنید",
"Allow users to publish their data to a global and public address book" : "به کاربران اجازه دهید داده های خود را در یک دفترچه آدرس عمومی و عمومی منتشر کنند",
"Unable to update federated files sharing config" : "به‌روزرسانی پیکربندی اشتراک‌گذاری فایل‌های فدرال ممکن نیست",
"Federated Cloud" : "ابر خودگردان",
"You can share with anyone who uses a Nextcloud server or other Open Cloud Mesh (OCM) compatible servers and services! Just put their Federated Cloud ID in the share dialog. It looks like person@cloud.example.com" : "می‌توانید با هر کسی که از سرور Nextcloud یا سایر سرورها و سرویس‌های سازگار با Open Cloud Mesh (OCM) استفاده می‌کند، اشتراک‌گذاری کنید! فقط شناسه ابری فدرال آنها را در گفتگوی اشتراک گذاری قرار دهید. به نظر می رسد person@cloud.example.com",
"Your Federated Cloud ID:" : "شناسهٔ ابردی خودگردانتان:",
"Share it so your friends can share files with you:" : "آن را به اشتراک بگذارید تا دوستانتان بتوانند فایل ها را با شما به اشتراک بگذارند:",
"Facebook" : "فیس‌بوک",
"Twitter" : "توییتر",
"Diaspora" : "دیازپورا",
"Add to your website" : "افزودن به پایگاه وبتان",
"Share with me via Nextcloud" : "هم‌رسانی با من روی نسکت‌کلود",
"HTML Code:" : "کد HTML :",
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "از طریق شناسه ابری فدرال #Nextcloud با من به اشتراک بگذارید، به {url} مراجعه کنید",
"Share with me through my #Nextcloud Federated Cloud ID" : "از طریق شناسه ابری فدرال #Nextcloud با من به اشتراک بگذارید",
"Cloud ID copied to the clipboard" : "Cloud ID در کلیپ بورد کپی شد",
"Copy to clipboard" : "رونوشت به تخته‌گیره",
"Clipboard is not available" : "تخته گیره موحود نیست",
"Copied!" : "رونوشت شد!"
"Copied!" : "رونوشت شد!",
"You received \"%3$s\" as a remote share from %4$s (%1$s) (on behalf of %5$s (%2$s))" : "شما \"%3$s\" را به عنوان یک اشتراک راه دور از %4$s(%1$s) (از طرف%5$s (%2$s)) دریافت کردید.",
"You received \"%3$s\" as a remote share from %4$s (%1$s)" : "شما \"%3$s\" را به عنوان یک اشتراک راه دور از %4$s(%1$s) دریافت کردید",
"Share with me through my #Nextcloud Federated Cloud ID, see %s" : "از طریق شناسه ابری فدرال #Nextcloud با من به اشتراک بگذارید، ببینید%s"
},
"nplurals=2; plural=(n > 1);");

View file

@ -6,26 +6,52 @@
"Add remote share" : "افزودن هم‌رسانی دوردست",
"Invalid Federated Cloud ID" : "شناسهٔ ابری خودگردان نامعتبر",
"Server to server sharing is not enabled on this server" : "هم‌رسانی کارساز به کارساز روی این کارساز به کار نیفتاده",
"Couldn't establish a federated share." : "نمی توان یک سهم فدرال ایجاد کرد.",
"Couldn't establish a federated share, maybe the password was wrong." : "نمی‌توان یک اشتراک فدرال ایجاد کرد، شاید رمز عبور اشتباه بوده است.",
"Federated Share request sent, you will receive an invitation. Check your notifications." : "درخواست اشتراک فدرال ارسال شد، یک دعوت نامه دریافت خواهید کرد. اعلان های خود را بررسی کنید.",
"Couldn't establish a federated share, it looks like the server to federate with is too old (Nextcloud <= 9)." : "نمی‌توان یک اشتراک فدرال ایجاد کرد، به نظر می‌رسد سروری که باید با آن فدرال شود خیلی قدیمی است (Nextcloud <= 9).",
"It is not allowed to send federated group shares from this server." : "ارسال اشتراک های گروه فدرال از این سرور مجاز نیست.",
"Sharing %1$s failed, because this item is already shared with user %2$s" : "هم‌رسانی %1$s شکست خورد، چرا که این مورد از پیش با کاربر%2$s هم رسانی شده بود",
"Not allowed to create a federated share with the same user" : "ایجاد یک هم‌رسانی خودگران با همان کاربر مجاز نیست",
"Federated shares require read permissions" : "سهام فدرال به مجوز خواندن نیاز دارد",
"File is already shared with %s" : "فایل قبلاً با به اشتراک گذاشته شده است%s",
"Sharing %1$s failed, could not find %2$s, maybe the server is currently unreachable or uses a self-signed certificate." : "اشتراک‌گذاری %1$s انجام نشد، پیدا نشد%2$s، شاید سرور در حال حاضر غیرقابل دسترسی باشد یا از گواهی امضا شده استفاده می‌کند.",
"Could not find share" : "نتوانست هم‌رسانی را بیابد",
"Federated sharing" : "هم‌رسانی خودگردان",
"You received {share} as a remote share from {user} (on behalf of {behalf})" : "شما {share} را به عنوان یک اشتراک راه دور از {user} دریافت کردید (از طرف {behalf})",
"You received {share} as a remote share from {user}" : "شما {share} را به عنوان اشتراک راه دور از {user} دریافت کردید",
"Accept" : "قبول",
"Decline" : "کاهش می یابد",
"Federated Cloud Sharing" : "هم‌رسانی ابری خودگردان",
"Sharing" : "هم‌رسانی",
"Federated file sharing" : "هم‌رسانی پروندهٔ خودگردان",
"Provide federated file sharing across servers" : "فراهم‌کنندهٔ هم‌رسانی پروندهٔ خودگردان میان کارسازها",
"Adjust how people can share between servers. This includes shares between users on this server as well if they are using federated sharing." : "نحوه اشتراک گذاری افراد بین سرورها را تنظیم کنید. این شامل اشتراک گذاری بین کاربران در این سرور نیز می شود، در صورتی که از اشتراک گذاری فدرال استفاده می کنند.",
"Allow users on this server to send shares to other servers (this option also allows WebDAV access to public shares)" : "به کاربران این سرور اجازه می‌دهد تا اشتراک‌ها را به سرورهای دیگر ارسال کنند (این گزینه همچنین به WebDAV اجازه دسترسی به اشتراک‌های عمومی را می‌دهد)",
"Allow users on this server to receive shares from other servers" : "به کاربران موجود در این سرور اجازه دهید از سرورهای دیگر اشتراک‌گذاری دریافت کنند",
"Allow users on this server to send shares to groups on other servers" : "به کاربران موجود در این سرور امکان ارسال اشتراک‌ها به گروه‌های موجود در سرورهای دیگر را بدهید",
"Allow users on this server to receive group shares from other servers" : "به کاربران این سرور اجازه دهید اشتراک‌های گروهی را از سرورهای دیگر دریافت کنند",
"Search global and public address book for users" : "کتاب آدرس عمومی و عمومی را برای کاربران جستجو کنید",
"Allow users to publish their data to a global and public address book" : "به کاربران اجازه دهید داده های خود را در یک دفترچه آدرس عمومی و عمومی منتشر کنند",
"Unable to update federated files sharing config" : "به‌روزرسانی پیکربندی اشتراک‌گذاری فایل‌های فدرال ممکن نیست",
"Federated Cloud" : "ابر خودگردان",
"You can share with anyone who uses a Nextcloud server or other Open Cloud Mesh (OCM) compatible servers and services! Just put their Federated Cloud ID in the share dialog. It looks like person@cloud.example.com" : "می‌توانید با هر کسی که از سرور Nextcloud یا سایر سرورها و سرویس‌های سازگار با Open Cloud Mesh (OCM) استفاده می‌کند، اشتراک‌گذاری کنید! فقط شناسه ابری فدرال آنها را در گفتگوی اشتراک گذاری قرار دهید. به نظر می رسد person@cloud.example.com",
"Your Federated Cloud ID:" : "شناسهٔ ابردی خودگردانتان:",
"Share it so your friends can share files with you:" : "آن را به اشتراک بگذارید تا دوستانتان بتوانند فایل ها را با شما به اشتراک بگذارند:",
"Facebook" : "فیس‌بوک",
"Twitter" : "توییتر",
"Diaspora" : "دیازپورا",
"Add to your website" : "افزودن به پایگاه وبتان",
"Share with me via Nextcloud" : "هم‌رسانی با من روی نسکت‌کلود",
"HTML Code:" : "کد HTML :",
"Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "از طریق شناسه ابری فدرال #Nextcloud با من به اشتراک بگذارید، به {url} مراجعه کنید",
"Share with me through my #Nextcloud Federated Cloud ID" : "از طریق شناسه ابری فدرال #Nextcloud با من به اشتراک بگذارید",
"Cloud ID copied to the clipboard" : "Cloud ID در کلیپ بورد کپی شد",
"Copy to clipboard" : "رونوشت به تخته‌گیره",
"Clipboard is not available" : "تخته گیره موحود نیست",
"Copied!" : "رونوشت شد!"
"Copied!" : "رونوشت شد!",
"You received \"%3$s\" as a remote share from %4$s (%1$s) (on behalf of %5$s (%2$s))" : "شما \"%3$s\" را به عنوان یک اشتراک راه دور از %4$s(%1$s) (از طرف%5$s (%2$s)) دریافت کردید.",
"You received \"%3$s\" as a remote share from %4$s (%1$s)" : "شما \"%3$s\" را به عنوان یک اشتراک راه دور از %4$s(%1$s) دریافت کردید",
"Share with me through my #Nextcloud Federated Cloud ID, see %s" : "از طریق شناسه ابری فدرال #Nextcloud با من به اشتراک بگذارید، ببینید%s"
},"pluralForm" :"nplurals=2; plural=(n > 1);"
}

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["files.scss","../../../core/css/functions.scss"],"names":[],"mappings":"AAWA,SAEC,YACA,YACA,qBACA,WAED,oEACA,8BACA,kDAEC,+CAED,0BACC,oDAGD,mBACC,kBACA,aACA,SACA,4CACC,iBAIF,gBACC,aAGD,OACC,iBACA,YACA,aACA,aACA,mBAGD,6EAGC,yBACA,gCAID,kBACC,kBACA,WACA,gBACA,cACA,sBAEA,6CACC,aAGD,wBACC,wBACA,gBAEA,SAEA,WACA,cACA,0DAMD,wBACC,cACA,WAEA,mGAEC,8CAEA,6KACC,oCAKF,8DACC,oBAKH,yBACC,aAID,uCACC,cACA,WAGD,wBAGC,yBAEA,qBAGD,6FACC,+DAGD,iCACC,yDAGD,kFACC,0CAGD,4EACC,+DAID,gBCxEC,yCD2ED,iBC3EC,yCD8ED,oBC9EC,0CDiFD,yFCjFC,wCDuFD,uBCvFC,yCD0FD,2BC1FC,2CD6FD,mBC7FC,yCDgGD,2BChGC,4CDmGD,wBCnGC,0CDsGD,4BCtGC,4CD0GD,4CACC,WAGD,iCACC,WACA,YACA,eACA,SACA,eAGD,wCACC,aAGD,0CACC,WAGD,2BACC,YAED,4KAKC,+CAED,wMAKC,oDAGD,qCAEA,yDACC,oCAED,kCACC,iCACA,8BACA,4BACA,yBACA,mBAED,wGAIC,UACA,oCAGD,oBACC,oCAED,uBACC,6BAED,sBACC,cACA,aACA,YACA,sBACA,2BACA,sBACA,oCACC,kBAGF,kCACC,qBACA,mBAED,2BACC,eACA,iBAGD,uCACC,cAGD,yBACC,WACA,WACA,gBACA,qBACA,2BACA,WAED,wJAIC,kBAED,2CACC,eAED,4EAEC,mBAGD,kBAEC,4CACA,gBACA,mBAED,SACC,eACA,kBACA,+BACA,4BAED,qBACC,kBACA,aACA,UAGD,uBACC,kBACA,YAGD,0BACC,gBAED,uCACC,iBAED,8EAEC,2BACA,sBACA,kBAEA,gBAGD,qMAQC,gBACA,qPACC,MAIF,2BACC,0DACA,iBAGD,sDACC,iBAGD,+BACC,kBACA,aAED,kCACC,aAGD,0DAGC,WACA,kBAED,kDAEC,aACA,kBACA,2BACA,sBACA,YACA,iBACA,UAED,qCAEC,QACA,eACA,eACA,YAGA,8DACC,WAED,mEACC,WAGF,6BACC,qBACA,WACA,YACA,wBACA,2BACA,4BACA,gBACA,eACA,mCACA,eACA,kBACA,UAED,oCACC,eAID,2CACC,qCAGD,iDACC,qBACA,4BACA,YAED,uBACC,iBACA,kBACA,SAGD,6IACA,8FAEA,wCACC,kBACA,gBACA,uBACA,YAKA,kBACC,YACA,4BACC,QACA,YACA,aACA,gBACA,mBACA,uBACA,YACA,WACA,mBAID,+BACC,iBACA,aACA,uBACA,mBACA,cACA,kCACA,gBAEA,iDACC,iBACA,iBACA,wCACA,iCACA,oCACA,uBACA,mBACA,gBACA,uBACA,iBACA,kBAEA,uDACC,iBACA,sBAID,mEACC,gBAOL,iJAEC,wBAGD,mCACC,iCACA,8BACA,4BACA,yBAED,4BACC,WAGD,2CACC,uBACA,gBACA,kBACA,mBAKD,8BACC,kBACA,mBAEA,iBACA,OACA,SACA,YACA,cAEA,iBACA,eAEA,iBACA,oCACA,uBACA,mBAGD,mBACC,UAID,6DACC,WACA,eAID,iRAIC,UAID,0EACC,WAMA,wEACC,aAGD,oGACC,+CACA,wCACA,wBACA,yDACA,aAIF,oGAEC,mBAGD,+BACC,kBACA,WACA,eACA,gBACA,wJAGD,wFAEC,kBACA,UACA,YAGD,yCACC,qBACA,WAED,8CACC,kBACA,cACA,SACA,WACA,iBACA,kBAGD,iDAGA,aACC,WAGD,iCACC,kBAID,mDAEC,gBAID,oCACC,qBACA,0BAGD,8EACC,0BAOA,kCACC,eAGD,sEACC,eAGD,sCACC,gBAIF,aACC,YACA,WACA,2BAGD,qCACC,wCAID,iBACI,kBACA,qBACA,sBAEJ,wBACI,aAEJ,mBACC,eACA,iBACA,iBAGD,0BACC,aAED,uBACC,kBACA,2BACA,mBAGD,8CACC,gBAIA,8BACC,eACA,iBACA,iBACA,WACA,2CACC,kBACA,0FAGC,kBACA,cACA,SACA,UACA,WACA,gBAED,mDACC,qBACA,sBAGF,0CACC,iBACA,oBACA,kBACA,mBAGA,oGACC,WAID,qIAEC,WAED,uDACC,WACA,0HACC,WAIH,wEACC,UAED,oCACC,+CACA,wCAGF,uGACC,WAED,wDACC,UAKF,4EACC,qBACA,eACA,gBACA,uBACA,sBACA,gBAGD,2CACC,yBAGD,yCACC,UAGD,kNAKC,UAGD,qCACC,gBAGD,0FAEC,WAGD,mDACC,eAGD,SACC,oCAGA,aAED,wCACC,WAEA,mBAKD,sBACC,aAED,2DAIC,+BAED,YACC,mBACA,mBACA,iBAED,wBACC,UAED,YACC,qBAGD,iBACC,WACA,aAED,6BACC,kBACA,mBACA,YAGA,gBAED,yBACC,kBAED,MACC,WACA,kBACA,MACA,OACA,QACA,SACA,8CACA,sCACA,wBACA,WACA,yBACA,8BACA,4BACA,6BACA,iCAED,kBACC,UAGD,aACC,gBACA,SACA,sBACA,eACA,gBACA,aAGA,oBACC,qBAKF,gBACC,sBACA,wBACA,gBACA,YACA,UACA,SACA,0DACA,WACA,yBACA,sBACA,qBACA,iBACA,aACA,MACA,kBAKE,0IACC,sBACA,qBACA,aACA,YACA,WACA,YACA,mBACA,uBAED,oFACC,aAQJ,0DACC,OAGD,6KAIC,qBACA,sBACA,0BAMA,sDACC,sBAED,yDACC,uDAIF,iJAGC,aAGD,oJAGC,WACA,YAGD,gCACC,kBAGD,YACC,mBAEA,uBACC,mCAIF,0DAEC,oCAED,qBACC,oCACA,4BACC,2BAIF,cACC,iBACA,kBACA,gBACA,6BACA,cACA,gBACA,YAEA,2BACC,aAGD,kCACC,UACA,kBACA,iBAIF,uBACC,oBACA,YACA,gBACA,+BACA,UACA,YACA,wBACA,sBAEA,6BACC,YAKA,oEACC,0BAIF,kCACC,WACA,mCAWA,kDACC,cACA,4CACA,0DACA,qDACC,WACA,YAMH,+CACC,aACA,+CACA,6BACA,aACA,cAGA,+DACC,cACA,kBACA,aACA,mCAEA,0fAKC,+BAEA,oxDAGC,+CAKH,kDACC,eACA,mBAGC,8EACC,YACA,eACA,kBACA,MAvDQ,MAwDR,OAxDQ,MAyDR,QAxDO,KAyDP,MACA,OACA,WAEA,yFACC,4BACA,6BACA,wBACA,SACA,mCACA,4BACA,2BAKA,wGACC,QA1EK,KA2EL,UACA,UACA,YAKH,uEACC,WACA,SACA,MACA,YAEA,YACA,gBAEA,kBAGD,iEACC,YACA,mCAIA,gBAKA,0BAEA,2EACC,aACA,YACA,iBACA,kBACA,iBACA,UAEA,0FACC,qBACA,kBACA,gBACA,uBACA,mBAED,kFACC,WACA,OACA,eAED,iFACC,WACA,OACA,eAID,sFACC,aAKF,8EACC,aAGD,8EACC,eACA,iBACA,aACA,mBACA,kBACA,QAEA,sFACC,QAzJK,KA0JL,WACA,YACA,aACA,mBACA,uBAGA,wGACC,aAQH,2GACC,yBAEA,6HACC,YACA,kBAIF,6GACC,yBAGD,6GACC,yBAIF,gEACC,iBACA,mCAEA,+EACC,WACA,cACA,YAMH,kHAEC,aAGD,sIAEC,kBACA,SACA,UACA,aACA,WACA,YACA,WACA,yBAEA,kJACC,WACA,YACA,oBACA,QA7NO,KA8NP,kKACC,SACA,MAhOM,KAiON,OAjOM,KAuOT,+DACC,OACA,YACA,aAGA,yFACC,gBACA,uBAMJ,+FACC,cAID,+CACC,aAEA,qEACC,qBACA,cAEA,aAEA,wEACC,iBAEA,iKAEC,aAGD,8EACI,cAQR,aACC,0DACA,YACA,SACA,aACA,WACA,YACA,mCACA,iCACA,YACA,gBAEA,uEAGC,UAGD,oEAEC,mEASF,cACC,eACA,MAOC,uGACC,gBAID,4EACC,WAKF,0BACC,kBACA,QACA,MAKF,gBACC,aAGD,8BACC,gBACA,sBACA,kBACA,kBACA,aACA,eACA,mBAEA,iCACC,WACA,eAGD,6DACC,aACA,YACA","file":"files.css"}
{"version":3,"sourceRoot":"","sources":["files.scss","../../../core/css/functions.scss"],"names":[],"mappings":"AAWA,SAEC,YACA,YACA,qBACA,WAED,oEACA,8BACA,kDAEC,+CAED,0BACC,oDAGD,mBACC,kBACA,aACA,SACA,4CACC,iBAIF,gBACC,aAGD,OACC,iBACA,YACA,aACA,aACA,mBAGD,6EAGC,yBACA,gCAID,kBACC,kBACA,WACA,gBACA,cACA,sBAEA,6CACC,aAGD,wBACC,wBACA,gBAEA,SAEA,WACA,cACA,0DAMD,wBACC,cACA,WAEA,mGAEC,8CAEA,6KACC,oCAKF,8DACC,oBAKH,yBACC,aAID,uCACC,cACA,WAGD,wBAGC,yBAEA,qBAGD,6FACC,+DAGD,iCACC,yDAGD,kFACC,0CAGD,4EACC,+DAID,gBCxEC,yCD2ED,iBC3EC,yCD8ED,oBC9EC,0CDiFD,yFCjFC,wCDuFD,uBCvFC,yCD0FD,2BC1FC,2CD6FD,mBC7FC,yCDgGD,2BChGC,4CDmGD,wBCnGC,0CDsGD,4BCtGC,4CD0GD,4CACC,WAGD,iCACC,WACA,YACA,eACA,SACA,eAGD,wCACC,aAGD,0CACC,WAGD,2BACC,YAED,4KAKC,+CAED,wMAKC,oDAGD,qCAEA,yDACC,oCAED,kCACC,iCACA,8BACA,4BACA,yBACA,mBAED,wGAIC,UACA,oCAGD,oBACC,oCAED,uBACC,6BAED,sBACC,cACA,aACA,YACA,sBACA,2BACA,sBACA,oCACC,kBAGF,kCACC,qBACA,mBAED,2BACC,eACA,iBAGD,uCACC,cAGD,yBACC,WACA,WACA,gBACA,qBACA,2BACA,WAED,wJAIC,kBAED,2CACC,eAED,4EAEC,mBAGD,kBAEC,4CACA,gBACA,mBAED,SACC,eACA,kBACA,+BACA,4BAED,qBACC,kBACA,aACA,UAGD,uBACC,kBACA,YAGD,0BACC,gBAED,uCACC,iBAED,8EAEC,2BACA,sBACA,kBAEA,gBAGD,qMAQC,gBACA,qPACC,MAIF,2BACC,0DACA,iBAGD,sDACC,iBAGD,+BACC,kBACA,aAED,kCACC,aAGD,0DAGC,WACA,kBAED,kDAEC,aACA,kBACA,2BACA,sBACA,YACA,iBACA,UAED,qCAEC,QACA,eACA,eACA,YAGA,8DACC,WAED,mEACC,WAGF,6BACC,qBACA,WACA,YACA,wBACA,2BACA,4BACA,gBACA,eACA,mCACA,eACA,kBACA,UAED,oCACC,eAID,2CACC,qCAGD,iDACC,qBACA,4BACA,YAED,uBACC,iBACA,kBACA,SAGD,6IACA,8FAEA,wCACC,kBACA,gBACA,uBACA,YAKA,kBACC,YACA,4BACC,QACA,YACA,aACA,gBACA,mBACA,uBACA,YACA,WACA,mBAID,+BACC,iBACA,aACA,uBACA,mBACA,cACA,kCACA,gBAEA,iDACC,iBACA,iBACA,wCACA,iCACA,oCACA,uBACA,mBACA,gBACA,uBACA,iBACA,kBAEA,uDACC,iBACA,sBAID,mEACC,gBAOL,iJAEC,wBAGD,mCACC,iCACA,8BACA,4BACA,yBAED,4BACC,WAGD,2CACC,uBACA,gBACA,kBACA,mBAKD,8BACC,kBACA,mBAEA,iBACA,OACA,SACA,YACA,cAEA,iBACA,eAEA,iBACA,oCACA,uBACA,mBAGD,mBACC,UAID,6DACC,WACA,eAID,iRAIC,UAID,0EACC,WAMA,wEACC,aAGD,oGACC,+CACA,wCACA,wBACA,yDACA,aAIF,oGAEC,mBAGD,+BACC,kBACA,WACA,eACA,gBACA,wJAGD,wFAEC,kBACA,UACA,YAGD,yCACC,qBACA,WAED,8CACC,kBACA,cACA,SACA,WACA,iBACA,kBAGD,iDAGA,aACC,WAGD,iCACC,kBAID,mDAEC,gBAID,oCACC,qBACA,0BAGD,8EACC,0BAOA,kCACC,eAGD,sEACC,eAGD,sCACC,gBAIF,aACC,YACA,WACA,2BAKA,0EACC,wCAKF,iBACI,kBACA,qBACA,sBAEJ,wBACI,aAEJ,mBACC,eACA,iBACA,iBAGD,0BACC,aAED,uBACC,kBACA,2BACA,mBAGD,8CACC,gBAIA,8BACC,eACA,iBACA,iBACA,WACA,2CACC,kBACA,0FAGC,kBACA,cACA,SACA,UACA,WACA,gBAED,mDACC,qBACA,sBAGF,0CACC,iBACA,oBACA,kBACA,mBAGA,oGACC,WAID,qIAEC,WAED,uDACC,WACA,0HACC,WAIH,wEACC,UAED,oCACC,+CACA,wCAGF,uGACC,WAED,wDACC,UAKF,4EACC,qBACA,eACA,gBACA,uBACA,sBACA,gBAGD,2CACC,yBAGD,yCACC,UAGD,kNAKC,UAGD,qCACC,gBAGD,0FAEC,WAGD,mDACC,eAGD,SACC,oCAGA,aAED,wCACC,WAEA,mBAKD,sBACC,aAED,2DAIC,+BAED,YACC,mBACA,mBACA,iBAED,wBACC,UAED,YACC,qBAGD,iBACC,WACA,aAED,6BACC,kBACA,mBACA,YAGA,gBAED,yBACC,kBAED,MACC,WACA,kBACA,MACA,OACA,QACA,SACA,8CACA,sCACA,wBACA,WACA,yBACA,8BACA,4BACA,6BACA,iCAED,kBACC,UAGD,aACC,gBACA,SACA,sBACA,eACA,gBACA,aAGA,oBACC,qBAKF,gBACC,sBACA,wBACA,gBACA,YACA,UACA,SACA,0DACA,WACA,yBACA,sBACA,qBACA,iBACA,aACA,MACA,kBAKE,0IACC,sBACA,qBACA,aACA,YACA,WACA,YACA,mBACA,uBAED,oFACC,aAQJ,0DACC,OAGD,6KAIC,qBACA,sBACA,0BAMA,sDACC,sBAED,yDACC,uDAIF,iJAGC,aAGD,oJAGC,WACA,YAGD,gCACC,kBAGD,YACC,mBAEA,uBACC,mCAIF,0DAEC,oCAED,qBACC,oCACA,4BACC,2BAIF,cACC,iBACA,kBACA,gBACA,6BACA,cACA,gBACA,YAEA,2BACC,aAGD,kCACC,UACA,kBACA,iBAIF,uBACC,oBACA,YACA,gBACA,+BACA,UACA,YACA,wBACA,sBAEA,6BACC,YAKA,oEACC,0BAIF,kCACC,WACA,mCAWA,kDACC,cACA,4CACA,0DACA,qDACC,WACA,YAMH,+CACC,aACA,+CACA,6BACA,aACA,cAGA,+DACC,cACA,kBACA,aACA,mCAEA,0fAKC,+BAEA,oxDAGC,+CAKH,kDACC,eACA,mBAGC,8EACC,YACA,eACA,kBACA,MAvDQ,MAwDR,OAxDQ,MAyDR,QAxDO,KAyDP,MACA,OACA,WAEA,yFACC,4BACA,6BACA,wBACA,SACA,mCACA,4BACA,2BAKA,wGACC,QA1EK,KA2EL,UACA,UACA,YAKH,uEACC,WACA,SACA,MACA,YAEA,YACA,gBAEA,kBAGD,iEACC,YACA,mCAIA,gBAKA,0BAEA,2EACC,aACA,YACA,iBACA,kBACA,iBACA,UAEA,0FACC,qBACA,kBACA,gBACA,uBACA,mBAED,kFACC,WACA,OACA,eAED,iFACC,WACA,OACA,eAID,sFACC,aAKF,8EACC,aAGD,8EACC,eACA,iBACA,aACA,mBACA,kBACA,QAEA,sFACC,QAzJK,KA0JL,WACA,YACA,aACA,mBACA,uBAGA,wGACC,aAQH,2GACC,yBAEA,6HACC,YACA,kBAIF,6GACC,yBAGD,6GACC,yBAIF,gEACC,iBACA,mCAEA,+EACC,WACA,cACA,YAMH,kHAEC,aAGD,sIAEC,kBACA,SACA,UACA,aACA,WACA,YACA,WACA,yBAEA,kJACC,WACA,YACA,oBACA,QA7NO,KA8NP,kKACC,SACA,MAhOM,KAiON,OAjOM,KAuOT,+DACC,OACA,YACA,aAGA,yFACC,gBACA,uBAMJ,+FACC,cAID,+CACC,aAEA,qEACC,qBACA,cAEA,aAEA,wEACC,iBAEA,iKAEC,aAGD,8EACI,cAQR,aACC,0DACA,YACA,SACA,aACA,WACA,YACA,mCACA,iCACA,YACA,gBAEA,uEAGC,UAGD,oEAEC,mEASF,cACC,eACA,MAOC,uGACC,gBAID,4EACC,WAKF,0BACC,kBACA,QACA,MAKF,gBACC,aAGD,8BACC,gBACA,sBACA,kBACA,kBACA,aACA,eACA,mBAEA,iCACC,WACA,eAGD,6DACC,aACA,YACA","file":"files.css"}

View file

@ -641,8 +641,11 @@ a.action > img {
vertical-align: text-bottom;
}
a.action.action-editlocally img.icon {
filter: var(--background-invert-if-dark);
a.action.action-editlocally,
a.action.action-setreminder {
img.icon {
filter: var(--background-invert-if-dark);
}
}
/* Actions for selected files */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -710,6 +710,22 @@
}
});
if (Boolean(OC.appswebroots.files_reminders) && Boolean(OC.appswebroots.notifications)) {
this.registerAction({
name: 'SetReminder',
displayName: function(_context) {
return t('files', 'Set reminder');
},
mime: 'all',
order: -24,
icon: function(_filename, _context) {
return OC.imagePath('files_reminders', 'alarm.svg')
},
permissions: OC.PERMISSION_READ,
actionHandler: function(_filename, _context) {},
});
}
if (!/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
this.registerAction({
name: 'EditLocally',

View file

@ -133,6 +133,11 @@
this.render();
this.$el.removeClass('hidden');
window._nc_event_bus.emit('files:action-menu:opened', {
el: this.$el[0],
context,
})
OC.showMenu(null, this.$el);
}
});
@ -140,4 +145,3 @@
OCA.Files.FileActionsMenu = FileActionsMenu;
})();

View file

@ -707,8 +707,10 @@
tr.addClass('highlighted');
this._currentFileModel = model;
const secondaryActionsOpen = Boolean(tr.find('.actions-secondary-vue').length)
// open sidebar and set file
if (typeof show === 'undefined' || !!show || (OCA.Files.Sidebar.file !== '')) {
if (!secondaryActionsOpen && (typeof show === 'undefined' || !!show || (OCA.Files.Sidebar.file !== ''))) {
OCA.Files.Sidebar.open(path.replace('//', '/'))
}
},

View file

@ -32,6 +32,7 @@ OC.L10N.register(
"Move" : "نقل",
"Copy" : "نسخ",
"Choose target folder" : "اختر مكان المجلد",
"Set reminder" : "ضبط التذكير",
"Edit locally" : "تعديل محليًا",
"Open" : "افتح",
"Delete file" : "احذف الملف",

View file

@ -30,6 +30,7 @@
"Move" : "نقل",
"Copy" : "نسخ",
"Choose target folder" : "اختر مكان المجلد",
"Set reminder" : "ضبط التذكير",
"Edit locally" : "تعديل محليًا",
"Open" : "افتح",
"Delete file" : "احذف الملف",

View file

@ -32,6 +32,7 @@ OC.L10N.register(
"Move" : "Mou",
"Copy" : "Copia",
"Choose target folder" : "Trieu la carpeta de destinació",
"Set reminder" : "Defineix un recordatori",
"Edit locally" : "Edita localment",
"Open" : "Obre",
"Delete file" : "Suprimeix el fitxer",

View file

@ -30,6 +30,7 @@
"Move" : "Mou",
"Copy" : "Copia",
"Choose target folder" : "Trieu la carpeta de destinació",
"Set reminder" : "Defineix un recordatori",
"Edit locally" : "Edita localment",
"Open" : "Obre",
"Delete file" : "Suprimeix el fitxer",

View file

@ -107,6 +107,8 @@ OC.L10N.register(
"Create new folder" : "Opret ny mappe",
"Upload file" : "Upload fil",
"Recent" : "Seneste",
"This file has the tag {tag}" : "Denne fil har tagget {tag}",
"This file has the tags {firstTags} and {lastTag}" : "Denne fil har taggene {firstTags} og {lastTag}",
"Not favorited" : "Ingen foretrukne",
"Remove from favorites" : "Fjern fra favoritter",
"Add to favorites" : "Tilføj til favoritter",
@ -166,6 +168,8 @@ OC.L10N.register(
"The ownership transfer of {path} from {user} has completed." : "Ejerskabsoverførslen af {path} fra {user} er fuldført.",
"in %s" : "i %s",
"File Management" : "Filhåndtering",
"Reload current directory" : "Genindlæs den aktuelle mappe",
"Go to the \"{dir}\" directory" : "Gå til mappen \"{dir}\"",
"Name cannot be empty" : "Navn må ikke være tomt",
"Select all" : "Vælg alle",
"Storage informations" : "Lagerinformationer",

View file

@ -105,6 +105,8 @@
"Create new folder" : "Opret ny mappe",
"Upload file" : "Upload fil",
"Recent" : "Seneste",
"This file has the tag {tag}" : "Denne fil har tagget {tag}",
"This file has the tags {firstTags} and {lastTag}" : "Denne fil har taggene {firstTags} og {lastTag}",
"Not favorited" : "Ingen foretrukne",
"Remove from favorites" : "Fjern fra favoritter",
"Add to favorites" : "Tilføj til favoritter",
@ -164,6 +166,8 @@
"The ownership transfer of {path} from {user} has completed." : "Ejerskabsoverførslen af {path} fra {user} er fuldført.",
"in %s" : "i %s",
"File Management" : "Filhåndtering",
"Reload current directory" : "Genindlæs den aktuelle mappe",
"Go to the \"{dir}\" directory" : "Gå til mappen \"{dir}\"",
"Name cannot be empty" : "Navn må ikke være tomt",
"Select all" : "Vælg alle",
"Storage informations" : "Lagerinformationer",

View file

@ -32,6 +32,7 @@ OC.L10N.register(
"Move" : "Verschieben",
"Copy" : "Kopieren",
"Choose target folder" : "Zielordner auswählen",
"Set reminder" : "Erinnerung erstellen",
"Edit locally" : "Lokal bearbeiten",
"Open" : "Öffnen",
"Delete file" : "Datei löschen",

View file

@ -30,6 +30,7 @@
"Move" : "Verschieben",
"Copy" : "Kopieren",
"Choose target folder" : "Zielordner auswählen",
"Set reminder" : "Erinnerung erstellen",
"Edit locally" : "Lokal bearbeiten",
"Open" : "Öffnen",
"Delete file" : "Datei löschen",

View file

@ -32,6 +32,7 @@ OC.L10N.register(
"Move" : "Move",
"Copy" : "Copy",
"Choose target folder" : "Choose target folder",
"Set reminder" : "Set reminder",
"Edit locally" : "Edit locally",
"Open" : "Open",
"Delete file" : "Delete file",

View file

@ -30,6 +30,7 @@
"Move" : "Move",
"Copy" : "Copy",
"Choose target folder" : "Choose target folder",
"Set reminder" : "Set reminder",
"Edit locally" : "Edit locally",
"Open" : "Open",
"Delete file" : "Delete file",

View file

@ -41,6 +41,10 @@ OC.L10N.register(
"Could not load info for file \"{file}\"" : "بارگیری اطلاعات برای پرونده امکان پذیر نیست \"{file}\"",
"Files" : "پرونده‌ها",
"Details" : "جزئیات",
"Please select tag(s) to add to the selection" : "لطفاً برچسب(های) را برای افزودن به انتخاب انتخاب کنید",
"Apply tag(s) to selection" : "تگ(ها) را در انتخاب اعمال کنید",
"Select directory \"{dirName}\"" : "دایرکتوری \"{dirName}\" را انتخاب کنید",
"Select file \"{fileName}\"" : "فایل \"{fileName}\" را انتخاب کنید",
"Pending" : "در انتظار",
"Unable to determine date" : "امکان تعیین تاریخ وجود ندارد",
"This operation is forbidden" : "این عملیات غیرمجاز است",
@ -53,35 +57,59 @@ OC.L10N.register(
"Could not copy \"{file}\"" : "پرونده کپی نشد",
"Copied {origin} inside {destination}" : "کپی شده (اصل) در مقصد",
"Copied {origin} and {nbfiles} other files inside {destination}" : "رونوشت شده از {origin} و {nbfiles} پرونده‌های دیگر در {destination}",
"Failed to redirect to client" : "هدایت به مشتری انجام نشد",
"{newName} already exists" : "{newName} قبلاً موجود است",
"Could not rename \"{fileName}\", it does not exist any more" : "نمی‌توان نام «{fileName}» را تغییر داد، دیگر وجود ندارد",
"The name \"{targetName}\" is already used in the folder \"{dir}\". Please choose a different name." : "عنوان \"{targetName}\" هم اکنون در پوشه \"{dir}\" وجود دارد. لطفا نام دیگری انتخاب کنید",
"Could not rename \"{fileName}\"" : "\"{fileName}\" تغییر نام داده نمی‌شود",
"Could not create file \"{file}\"" : "پروندهٔ \"{file}\" ساخته نشد",
"Could not create file \"{file}\" because it already exists" : "نمی توان پرونده \"{file}\" ایجاد کرد زیرا در حال حاضر وجود دارد",
"Could not create folder \"{dir}\" because it already exists" : "شاخه \"{dir}\" به علت موجود بودن ساخته نشد",
"Could not fetch file details \"{file}\"" : "جزئیات فایل \"{file}\" واکشی نشد",
"Error deleting file \"{fileName}\"." : "خطای حذف پروندهٔ \"{fileName}\".",
"No search results in other folders for {tag}{filter}{endtag}" : "جستجو در پوشه های دیگر برای {tag}{filter}{endtag} یافت نشد",
"Enter more than two characters to search in other folders" : "برای جستجو در پوشه های دیگر بیش از دو کاراکتر وارد کنید",
"Name" : "نام",
"Size" : "اندازه",
"Modified" : "تاریخ",
"_%n folder_::_%n folders_" : ["%n پوشه","%n پوشه"],
"_%n file_::_%n files_" : ["%n فایل","%n فایل"],
"{dirs} and {files}" : "{dirs} و {files}",
"_including %n hidden_::_including %n hidden_" : ["از جمله %nپنهان","از جمله %nپنهان"],
"You do not have permission to upload or create files here" : "شما اجازه آپلود یا ایجاد فایل در اینجا را ندارید",
"_Uploading %n file_::_Uploading %n files_" : ["در حال بارگذاری %n فایل","در حال بارگذاری %n فایل"],
"New file/folder menu" : "منوی فایل/پوشه جدید",
"Select file range" : "محدوده فایل را انتخاب کنید",
"{used}%" : "{used}%",
"{used} of {quota} used" : "{used} از {quota} استفاده شده",
"{used} used" : "{used} استفاده شده",
"\"{name}\" is an invalid file name." : "\"{name}\" نامی نامعتبر برای فایل است.",
"File name cannot be empty." : "نام پرونده نمی تواند خالی باشد.",
"\"/\" is not allowed inside a file name." : "\"/\" در داخل نام فایل مجاز نیست.",
"\"{name}\" is not an allowed filetype" : "\"{name}\" یک نوع پرونده مجاز نیست",
"Storage of {owner} is full, files cannot be updated or synced anymore!" : "فضای ذخیره‌سازی {owner} پر است، فایل‌ها دیگر نمی‌توانند به‌روزرسانی یا همگام‌سازی شوند!",
"Group folder \"{mountPoint}\" is full, files cannot be updated or synced anymore!" : "پوشه گروه \"{mountPoint}\" پر است، فایل‌ها دیگر قابل به‌روزرسانی یا همگام‌سازی نیستند!",
"External storage \"{mountPoint}\" is full, files cannot be updated or synced anymore!" : "حافظه خارجی \"{mountPoint}\" پر است، فایل‌ها دیگر نمی‌توانند به‌روزرسانی یا همگام‌سازی شوند!",
"Your storage is full, files cannot be updated or synced anymore!" : "فضای ذخیره‌سازی شما پر است، فایل‌ها دیگر نمی‌توانند به‌روزرسانی یا همگام‌سازی شوند!",
"Storage of {owner} is almost full ({usedSpacePercent}%)." : "فضای ذخیره سازی {owner} تقریباً پر است ({usedSpacePercent}%).",
"Group folder \"{mountPoint}\" is almost full ({usedSpacePercent}%)." : "پوشه گروه \"{mountPoint}\" تقریبا پر است ({usedSpacePercent}%).",
"External storage \"{mountPoint}\" is almost full ({usedSpacePercent}%)." : "حافظه خارجی \"{mountPoint}\" تقریباً پر است ({usedSpacePercent}%).",
"Your storage is almost full ({usedSpacePercent}%)." : "فضای ذخیره‌سازی شما تقریباً پر است ({usedSpacePercent}%).",
"_matches \"{filter}\"_::_match \"{filter}\"_" : ["مطابقت با \"{filter}\"","مطابقت با \"{filter}\""],
"View in folder" : "مشاهده در پوشه",
"Direct link was copied (only works for users who have access to this file/folder)" : "پیوند مستقیم کپی شد (فقط برای کاربرانی که به این فایل/پوشه دسترسی دارند کار می کند)",
"Path" : "مسیر",
"_%n byte_::_%n bytes_" : ["%n بایت","%n بایت"],
"Favorited" : "برگزیده شده",
"Favorite" : "برگزیده",
"Copy direct link (only works for users who have access to this file/folder)" : "کپی لینک مستقیم (فقط برای کاربرانی که به این فایل/پوشه دسترسی دارند کار می کند)",
"New folder" : "پوشه جدید",
"Create new folder" : "ساختن پوشه جدید",
"Upload file" : "بارگذاری پرونده",
"Recent" : "اخیر",
"This file has the tag {tag}" : "این فایل دارای تگ {tag} است",
"This file has the tags {firstTags} and {lastTag}" : "این فایل دارای تگ‌های {firstTags} و {lastTag} است.",
"Not favorited" : "مورد علاقه نیست",
"Remove from favorites" : "حذف کردن از برگزیده ها",
"Add to favorites" : "اضافه کردن به برگزیده ها",
"An error occurred while trying to update the tags" : "یک خطا در حین بروزرسانی برچسب‌ها رخ داده است",
@ -94,33 +122,137 @@ OC.L10N.register(
"Created by {user}" : "{user} ٖایجاد کرد",
"Changed by {user}" : "{user} تغییر داد",
"Deleted by {user}" : " {user} حذف کرد",
"Restored by {user}" : "بازیابی شده توسط {user}",
"Renamed by {user}" : "تغییر نام توسط {user}",
"Moved by {user}" : "منتقل شده توسط {user}",
"\"remote user\"" : "\"کاربران از راه‌ دور\"",
"You created {file}" : "شما {file} را ایجاد کردید",
"You created an encrypted file in {file}" : "شما یک فایل رمزگذاری شده در {file} ایجاد کردید",
"{user} created {file}" : "{user} {file} را ایجاد کرد",
"{user} created an encrypted file in {file}" : "{user} یک فایل رمزگذاری شده در {file} ایجاد کرد",
"{file} was created in a public folder" : "{file} در یک پوشه عمومی ایجاد شد",
"You changed {file}" : "شما {file} را تغییر دادید",
"You changed an encrypted file in {file}" : "شما یک فایل رمزگذاری شده را در {file} تغییر دادید",
"{user} changed {file}" : "{user} {file} را تغییر داد",
"{user} changed an encrypted file in {file}" : "{user} یک فایل رمزگذاری شده را در {file} تغییر داد",
"You deleted {file}" : "شما {file} را حذف کردید",
"You deleted an encrypted file in {file}" : "شما یک فایل رمزگذاری شده را در {file} حذف کردید",
"{user} deleted {file}" : "{user} {file} را حذف کرد",
"{user} deleted an encrypted file in {file}" : "{user} یک فایل رمزگذاری شده را در {file} حذف کرد",
"You restored {file}" : "شما {file} را بازیابی کردید",
"{user} restored {file}" : "{user} {file} را بازیابی کرد",
"You renamed {oldfile} (hidden) to {newfile} (hidden)" : "شما نام {oldfile} (پنهان) را به {newfile} (پنهان) تغییر دادید",
"You renamed {oldfile} (hidden) to {newfile}" : "شما نام {oldfile} (پنهان) را به {newfile} تغییر دادید",
"You renamed {oldfile} to {newfile} (hidden)" : "شما نام {oldfile} را به {newfile} تغییر دادید (پنهان)",
"You renamed {oldfile} to {newfile}" : "شما نام {oldfile} را به {newfile} تغییر دادید",
"{user} renamed {oldfile} (hidden) to {newfile} (hidden)" : "{user} به {oldfile} تغییر نام داد (پنهان) به {newfile} (مخفی)",
"{user} renamed {oldfile} (hidden) to {newfile}" : "{user} به {oldfile} تغییر نام داد (پنهان) به {newfile}",
"{user} renamed {oldfile} to {newfile} (hidden)" : "{user} نام {oldfile} را به {newfile} تغییر داد (پنهان)",
"{user} renamed {oldfile} to {newfile}" : "{user} نام {oldfile} را به {newfile} تغییر داد",
"You moved {oldfile} to {newfile}" : "شما {oldfile} را به {newfile} منتقل کردید",
"{user} moved {oldfile} to {newfile}" : "{user} {oldfile} را به {newfile} منتقل کرد",
"A file has been added to or removed from your <strong>favorites</strong>" : "یک فایل به موارد دلخواه شما اضافه یا حذف شده است",
"A file or folder has been <strong>changed</strong>" : "پرونده یا پوشه‌ای به <strong>تغییر</strong> داده شد",
"A favorite file or folder has been <strong>changed</strong>" : "یک فایل یا پوشه مورد علاقه تغییر کرده است",
"All files" : "تمامی فایل‌ها",
"Upload (max. %s)" : "آپلود (بیشترین سایز %s)",
"Accept" : "قبول",
"Reject" : "رد کردن",
"Incoming ownership transfer from {user}" : "انتقال مالکیت ورودی از {user}",
"Do you want to accept {path}?\n\nNote: The transfer process after accepting may take up to 1 hour." : "آیا می خواهید {path} را بپذیرید؟\n\nتوجه: فرآیند انتقال پس از پذیرش ممکن است تا 1 ساعت طول بکشد.",
"Ownership transfer failed" : "انتقال مالکیت ناموفق بود",
"Your ownership transfer of {path} to {user} failed." : "انتقال مالکیت شما از {path} به {user} انجام نشد.",
"The ownership transfer of {path} from {user} failed." : "انتقال مالکیت {path} از {user} انجام نشد.",
"Ownership transfer done" : "انتقال مالکیت انجام شد",
"Your ownership transfer of {path} to {user} has completed." : "انتقال مالکیت شما از {path} به {user} تکمیل شد.",
"The ownership transfer of {path} from {user} has completed." : "انتقال مالکیت {path} از {user} تکمیل شد.",
"in %s" : "در %s",
"File Management" : "مدیریت فایل",
"Reload current directory" : "دایرکتوری فعلی را دوباره بارگیری کنید",
"Go to the \"{dir}\" directory" : "به دایرکتوری \"{dir}\" بروید",
"Select the row for {displayName}" : "ردیف {displayName} را انتخاب کنید",
"Rename file" : "تغییر نام فایل",
"File name" : "نام فایل",
"A long time ago" : "مدت ها پیش",
"Download file {name}" : "دانلود فایل {name}",
"\"{displayName}\" action executed successfully" : "عملکرد \"{displayName}\" با موفقیت اجرا شد",
"\"{displayName}\" action failed" : "اقدام \"{displayName}\" ناموفق بود",
"\"{name}\" is not an allowed filetype." : "\"{name}\" یک نوع فایل مجاز نیست.",
"{newName} already exists." : "{newName} از قبل وجود دارد.",
"Name cannot be empty" : "نام نمی‌تواند خالی باشد",
"Another entry with the same name already exists" : "ورودی دیگری با همین نام در حال حاضر وجود دارد",
"Renamed \"{oldName}\" to \"{newName}\"" : "تغییر نام \"{oldName}\" به \"{newName}\"",
"Could not rename \"{oldName}\", it does not exist any more" : "نمی‌توان نام «{oldName}» را تغییر داد، دیگر وجود ندارد",
"The name \"{newName}\"\" is already used in the folder \"{dir}\". Please choose a different name." : "نام \"{newName}\" قبلاً در پوشه \"{dir}\" استفاده شده است. لطفاً نام دیگری انتخاب کنید.",
"Could not rename \"{oldName}\"" : "تغییر نام \"{oldName}\" ممکن نیست",
"Total rows summary" : "خلاصه کل ردیف ها",
"Select all" : "انتخاب همه",
"Unselect all" : "همه را لغو انتخاب کنید",
"\"{displayName}\" failed on some elements " : "\"{displayName}\" در برخی از عناصر ناموفق بود",
"\"{displayName}\" batch action executed successfully" : "عملکرد دسته‌ای \"{displayName}\" با موفقیت اجرا شد",
"ascending" : "صعودی",
"descending" : "نزولی",
"Sort list by {column} ({direction})" : "مرتب سازی لیست بر اساس {ستون} ({direction})",
"List of files and folders." : "لیست فایل ها و پوشه ها",
"This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "این لیست به دلایل عملکرد به طور کامل ارائه نشده است. در حین حرکت در لیست، فایل ها ارائه می شوند.",
"Storage informations" : "ذخیره سازی اطلاعات ",
"{usedQuotaByte} used" : "{usedQuotaByte} استفاده شده است",
"{relative}% used" : "{نسبی}% استفاده شده است",
"Could not refresh storage stats" : "نمی‌توان آمار ذخیره‌سازی را بازخوانی کرد",
"Transfer ownership of a file or folder" : "انتقال مالکیت یک فایل یا پوشه",
"Choose file or folder to transfer" : "فایل یا پوشه را برای انتقال انتخاب کنید",
"Change" : "تغییر",
"New owner" : "مالک جدید",
"Choose a file or folder to transfer" : "فایل یا پوشه ای را برای انتقال انتخاب کنید",
"Transfer" : "انتقال",
"Transfer {path} to {userid}" : "انتقال {path} به {userid}",
"Invalid path selected" : "مسیر نامعتبر انتخاب شده است",
"Unknown error" : "خطای نامشخص",
"Ownership transfer request sent" : "درخواست انتقال مالکیت ارسال شد",
"Cannot transfer ownership of a file or folder you do not own" : "نمی‌توان مالکیت فایل یا پوشه‌ای را که متعلق به شما نیست، منتقل کرد",
"Select file or folder to link to" : "فایل یا پوشه را برای پیوند انتخاب کنید",
"Loading current folder" : "در حال بارگیری پوشه فعلی",
"No files in here" : "هیچ فایلی اینجا وجود ندارد",
"Upload some content or sync with your devices!" : "محتوایی را آپلود کنید یا با دستگاه خود همگام‌سازی کنید!",
"Go to the previous folder" : "به پوشه قبلی بروید",
"Go back" : "برگرد",
"Open the files app settings" : "تنظیمات برنامه فایل ها را باز کنید",
"Files settings" : "تنظیمات پرونده‌ها",
"File cannot be accessed" : "فایل قابل دسترسی نیست",
"You might not have have permissions to view it, ask the sender to share it" : "ممکن است مجوز مشاهده آن را نداشته باشید، از فرستنده بخواهید آن را به اشتراک بگذارد",
"Sort favorites first" : "ابتدا موارد دلخواه را مرتب کنید",
"Show hidden files" : "نمایش پرونده‌های مخفی",
"Crop image previews" : "پیش نمایش تصویر برش",
"Additional settings" : "تنظیمات اضافی",
"WebDAV" : "WebDAV",
"Copy to clipboard" : "رونوشت به تخته‌گیره",
"Use this address to access your Files via WebDAV" : "از این آدرس برای دسترسی به فایل های خود از طریق WebDAV استفاده کنید",
"If you have enabled 2FA, you must create and use a new app password by clicking here." : "اگر 2FA را فعال کرده اید، باید با کلیک کردن در اینجا یک رمز عبور برنامه جدید ایجاد و استفاده کنید.",
"Clipboard is not available" : "تخته گیره موحود نیست",
"WebDAV URL copied to clipboard" : "URL WebDAV در کلیپ بورد کپی شد",
"Unable to change the favourite state of the file" : "امکان تغییر حالت دلخواه فایل وجود ندارد",
"Error while loading the file data" : "خطا هنگام بارگیری داده های فایل",
"Pick a template for {name}" : "یک الگو برای {name} انتخاب کنید",
"Create" : "ساخت",
"Create a new file with the selected template" : "یک فایل جدید با الگوی انتخاب شده ایجاد کنید",
"Creating file" : "ایجاد فایل",
"Blank" : "جای خالی",
"Unable to create new file from template" : "امکان ایجاد فایل جدید از الگو وجود ندارد",
"Delete permanently" : "حذف قطعی",
"Open folder {displayName}" : "باز کردن پوشه {displayName}",
"Open in Files" : "در فایل باز کنید",
"Open details" : "باز کردن جزئیات",
"Set up templates folder" : "پوشه قالب ها را تنظیم کنید",
"Templates" : "قالب‌ها",
"Create new templates folder" : "پوشه قالب های جدید ایجاد کنید",
"Unable to initialize the templates directory" : "راه اندازی دایرکتوری الگوها ممکن نیست",
"List of favorites files and folders." : "لیست فایل ها و پوشه های مورد علاقه",
"No favorites yet" : "هنوز مورد دلخواه وجود ندارد",
"Files and folders you mark as favorite will show up here" : "فایل‌ها و پوشه‌های انتخاب شده به عنوان برگزیده توسط شما، در اینجا نمایش داده می‌شود",
"List of recently modified files and folders." : "فهرست فایل‌ها و پوشه‌هایی که اخیراً اصلاح شده‌اند.",
"No recently modified files" : "هیچ فایلی که اخیراً اصلاح شده است",
"Files and folders you recently modified will show up here." : "فایل‌ها و پوشه‌هایی که اخیراً تغییر داده‌اید در اینجا نمایش داده می‌شوند.",
"Toggle %1$s sublist" : "تغییر%1$s فهرست فرعی",
"Toggle grid view" : "نمای شبکه را تغییر دهید",
"No entries found in this folder" : "هیچ ورودی‌ای در این پوشه وجود ندارد",
"Upload too large" : "سایز فایل برای آپلود زیاد است(م.تنظیمات در php.ini)",
@ -128,8 +260,12 @@ OC.L10N.register(
"Text file" : "فایل متنی",
"New text file.txt" : "پروندهٔ متنی جدید با پسوند txt",
"Storage invalid" : "فضای ذخیره‌سازی نامعتبر",
"You can only favorite a single file or folder at a time" : "در هر زمان فقط می توانید یک فایل یا پوشه را مورد علاقه خود قرار دهید",
"Unlimited" : "نامحدود",
"Search users" : "جستجوی کاربران",
"Cancel" : "لغو",
"%s used" : "%sاستفاده شده",
"%s%%" : "%s%%",
"%1$s of %2$s used" : "%1$s از %2$s استفاده شده ",
"Deleted files" : "پرونده‌های حذف شده",
"Shares" : "اشتراک گذاری ها",
@ -137,6 +273,10 @@ OC.L10N.register(
"Shared with you" : "Shared with you",
"Shared by link" : "اشتراک گذاشته شده از طریق لینک",
"Deleted shares" : "اشتراک گذاری های حذف شده",
"Pending shares" : "اشتراک در حال انتظار "
"Pending shares" : "اشتراک در حال انتظار ",
"Open folder {name}" : "باز کردن پوشه {name}",
"This list is not fully rendered for performances reasons. The files will be rendered as you navigate through the list." : "این لیست به دلایل اجرایی به طور کامل ارائه نشده است. در حین حرکت در لیست، فایل ها ارائه می شوند.",
"Search for an account" : "جستجو برای یک حساب کاربری",
"No files or folders have been deleted yet" : "هنوز هیچ فایل یا پوشه ای حذف نشده است"
},
"nplurals=2; plural=(n > 1);");

View file

@ -39,6 +39,10 @@
"Could not load info for file \"{file}\"" : "بارگیری اطلاعات برای پرونده امکان پذیر نیست \"{file}\"",
"Files" : "پرونده‌ها",
"Details" : "جزئیات",
"Please select tag(s) to add to the selection" : "لطفاً برچسب(های) را برای افزودن به انتخاب انتخاب کنید",
"Apply tag(s) to selection" : "تگ(ها) را در انتخاب اعمال کنید",
"Select directory \"{dirName}\"" : "دایرکتوری \"{dirName}\" را انتخاب کنید",
"Select file \"{fileName}\"" : "فایل \"{fileName}\" را انتخاب کنید",
"Pending" : "در انتظار",
"Unable to determine date" : "امکان تعیین تاریخ وجود ندارد",
"This operation is forbidden" : "این عملیات غیرمجاز است",
@ -51,35 +55,59 @@
"Could not copy \"{file}\"" : "پرونده کپی نشد",
"Copied {origin} inside {destination}" : "کپی شده (اصل) در مقصد",
"Copied {origin} and {nbfiles} other files inside {destination}" : "رونوشت شده از {origin} و {nbfiles} پرونده‌های دیگر در {destination}",
"Failed to redirect to client" : "هدایت به مشتری انجام نشد",
"{newName} already exists" : "{newName} قبلاً موجود است",
"Could not rename \"{fileName}\", it does not exist any more" : "نمی‌توان نام «{fileName}» را تغییر داد، دیگر وجود ندارد",
"The name \"{targetName}\" is already used in the folder \"{dir}\". Please choose a different name." : "عنوان \"{targetName}\" هم اکنون در پوشه \"{dir}\" وجود دارد. لطفا نام دیگری انتخاب کنید",
"Could not rename \"{fileName}\"" : "\"{fileName}\" تغییر نام داده نمی‌شود",
"Could not create file \"{file}\"" : "پروندهٔ \"{file}\" ساخته نشد",
"Could not create file \"{file}\" because it already exists" : "نمی توان پرونده \"{file}\" ایجاد کرد زیرا در حال حاضر وجود دارد",
"Could not create folder \"{dir}\" because it already exists" : "شاخه \"{dir}\" به علت موجود بودن ساخته نشد",
"Could not fetch file details \"{file}\"" : "جزئیات فایل \"{file}\" واکشی نشد",
"Error deleting file \"{fileName}\"." : "خطای حذف پروندهٔ \"{fileName}\".",
"No search results in other folders for {tag}{filter}{endtag}" : "جستجو در پوشه های دیگر برای {tag}{filter}{endtag} یافت نشد",
"Enter more than two characters to search in other folders" : "برای جستجو در پوشه های دیگر بیش از دو کاراکتر وارد کنید",
"Name" : "نام",
"Size" : "اندازه",
"Modified" : "تاریخ",
"_%n folder_::_%n folders_" : ["%n پوشه","%n پوشه"],
"_%n file_::_%n files_" : ["%n فایل","%n فایل"],
"{dirs} and {files}" : "{dirs} و {files}",
"_including %n hidden_::_including %n hidden_" : ["از جمله %nپنهان","از جمله %nپنهان"],
"You do not have permission to upload or create files here" : "شما اجازه آپلود یا ایجاد فایل در اینجا را ندارید",
"_Uploading %n file_::_Uploading %n files_" : ["در حال بارگذاری %n فایل","در حال بارگذاری %n فایل"],
"New file/folder menu" : "منوی فایل/پوشه جدید",
"Select file range" : "محدوده فایل را انتخاب کنید",
"{used}%" : "{used}%",
"{used} of {quota} used" : "{used} از {quota} استفاده شده",
"{used} used" : "{used} استفاده شده",
"\"{name}\" is an invalid file name." : "\"{name}\" نامی نامعتبر برای فایل است.",
"File name cannot be empty." : "نام پرونده نمی تواند خالی باشد.",
"\"/\" is not allowed inside a file name." : "\"/\" در داخل نام فایل مجاز نیست.",
"\"{name}\" is not an allowed filetype" : "\"{name}\" یک نوع پرونده مجاز نیست",
"Storage of {owner} is full, files cannot be updated or synced anymore!" : "فضای ذخیره‌سازی {owner} پر است، فایل‌ها دیگر نمی‌توانند به‌روزرسانی یا همگام‌سازی شوند!",
"Group folder \"{mountPoint}\" is full, files cannot be updated or synced anymore!" : "پوشه گروه \"{mountPoint}\" پر است، فایل‌ها دیگر قابل به‌روزرسانی یا همگام‌سازی نیستند!",
"External storage \"{mountPoint}\" is full, files cannot be updated or synced anymore!" : "حافظه خارجی \"{mountPoint}\" پر است، فایل‌ها دیگر نمی‌توانند به‌روزرسانی یا همگام‌سازی شوند!",
"Your storage is full, files cannot be updated or synced anymore!" : "فضای ذخیره‌سازی شما پر است، فایل‌ها دیگر نمی‌توانند به‌روزرسانی یا همگام‌سازی شوند!",
"Storage of {owner} is almost full ({usedSpacePercent}%)." : "فضای ذخیره سازی {owner} تقریباً پر است ({usedSpacePercent}%).",
"Group folder \"{mountPoint}\" is almost full ({usedSpacePercent}%)." : "پوشه گروه \"{mountPoint}\" تقریبا پر است ({usedSpacePercent}%).",
"External storage \"{mountPoint}\" is almost full ({usedSpacePercent}%)." : "حافظه خارجی \"{mountPoint}\" تقریباً پر است ({usedSpacePercent}%).",
"Your storage is almost full ({usedSpacePercent}%)." : "فضای ذخیره‌سازی شما تقریباً پر است ({usedSpacePercent}%).",
"_matches \"{filter}\"_::_match \"{filter}\"_" : ["مطابقت با \"{filter}\"","مطابقت با \"{filter}\""],
"View in folder" : "مشاهده در پوشه",
"Direct link was copied (only works for users who have access to this file/folder)" : "پیوند مستقیم کپی شد (فقط برای کاربرانی که به این فایل/پوشه دسترسی دارند کار می کند)",
"Path" : "مسیر",
"_%n byte_::_%n bytes_" : ["%n بایت","%n بایت"],
"Favorited" : "برگزیده شده",
"Favorite" : "برگزیده",
"Copy direct link (only works for users who have access to this file/folder)" : "کپی لینک مستقیم (فقط برای کاربرانی که به این فایل/پوشه دسترسی دارند کار می کند)",
"New folder" : "پوشه جدید",
"Create new folder" : "ساختن پوشه جدید",
"Upload file" : "بارگذاری پرونده",
"Recent" : "اخیر",
"This file has the tag {tag}" : "این فایل دارای تگ {tag} است",
"This file has the tags {firstTags} and {lastTag}" : "این فایل دارای تگ‌های {firstTags} و {lastTag} است.",
"Not favorited" : "مورد علاقه نیست",
"Remove from favorites" : "حذف کردن از برگزیده ها",
"Add to favorites" : "اضافه کردن به برگزیده ها",
"An error occurred while trying to update the tags" : "یک خطا در حین بروزرسانی برچسب‌ها رخ داده است",
@ -92,33 +120,137 @@
"Created by {user}" : "{user} ٖایجاد کرد",
"Changed by {user}" : "{user} تغییر داد",
"Deleted by {user}" : " {user} حذف کرد",
"Restored by {user}" : "بازیابی شده توسط {user}",
"Renamed by {user}" : "تغییر نام توسط {user}",
"Moved by {user}" : "منتقل شده توسط {user}",
"\"remote user\"" : "\"کاربران از راه‌ دور\"",
"You created {file}" : "شما {file} را ایجاد کردید",
"You created an encrypted file in {file}" : "شما یک فایل رمزگذاری شده در {file} ایجاد کردید",
"{user} created {file}" : "{user} {file} را ایجاد کرد",
"{user} created an encrypted file in {file}" : "{user} یک فایل رمزگذاری شده در {file} ایجاد کرد",
"{file} was created in a public folder" : "{file} در یک پوشه عمومی ایجاد شد",
"You changed {file}" : "شما {file} را تغییر دادید",
"You changed an encrypted file in {file}" : "شما یک فایل رمزگذاری شده را در {file} تغییر دادید",
"{user} changed {file}" : "{user} {file} را تغییر داد",
"{user} changed an encrypted file in {file}" : "{user} یک فایل رمزگذاری شده را در {file} تغییر داد",
"You deleted {file}" : "شما {file} را حذف کردید",
"You deleted an encrypted file in {file}" : "شما یک فایل رمزگذاری شده را در {file} حذف کردید",
"{user} deleted {file}" : "{user} {file} را حذف کرد",
"{user} deleted an encrypted file in {file}" : "{user} یک فایل رمزگذاری شده را در {file} حذف کرد",
"You restored {file}" : "شما {file} را بازیابی کردید",
"{user} restored {file}" : "{user} {file} را بازیابی کرد",
"You renamed {oldfile} (hidden) to {newfile} (hidden)" : "شما نام {oldfile} (پنهان) را به {newfile} (پنهان) تغییر دادید",
"You renamed {oldfile} (hidden) to {newfile}" : "شما نام {oldfile} (پنهان) را به {newfile} تغییر دادید",
"You renamed {oldfile} to {newfile} (hidden)" : "شما نام {oldfile} را به {newfile} تغییر دادید (پنهان)",
"You renamed {oldfile} to {newfile}" : "شما نام {oldfile} را به {newfile} تغییر دادید",
"{user} renamed {oldfile} (hidden) to {newfile} (hidden)" : "{user} به {oldfile} تغییر نام داد (پنهان) به {newfile} (مخفی)",
"{user} renamed {oldfile} (hidden) to {newfile}" : "{user} به {oldfile} تغییر نام داد (پنهان) به {newfile}",
"{user} renamed {oldfile} to {newfile} (hidden)" : "{user} نام {oldfile} را به {newfile} تغییر داد (پنهان)",
"{user} renamed {oldfile} to {newfile}" : "{user} نام {oldfile} را به {newfile} تغییر داد",
"You moved {oldfile} to {newfile}" : "شما {oldfile} را به {newfile} منتقل کردید",
"{user} moved {oldfile} to {newfile}" : "{user} {oldfile} را به {newfile} منتقل کرد",
"A file has been added to or removed from your <strong>favorites</strong>" : "یک فایل به موارد دلخواه شما اضافه یا حذف شده است",
"A file or folder has been <strong>changed</strong>" : "پرونده یا پوشه‌ای به <strong>تغییر</strong> داده شد",
"A favorite file or folder has been <strong>changed</strong>" : "یک فایل یا پوشه مورد علاقه تغییر کرده است",
"All files" : "تمامی فایل‌ها",
"Upload (max. %s)" : "آپلود (بیشترین سایز %s)",
"Accept" : "قبول",
"Reject" : "رد کردن",
"Incoming ownership transfer from {user}" : "انتقال مالکیت ورودی از {user}",
"Do you want to accept {path}?\n\nNote: The transfer process after accepting may take up to 1 hour." : "آیا می خواهید {path} را بپذیرید؟\n\nتوجه: فرآیند انتقال پس از پذیرش ممکن است تا 1 ساعت طول بکشد.",
"Ownership transfer failed" : "انتقال مالکیت ناموفق بود",
"Your ownership transfer of {path} to {user} failed." : "انتقال مالکیت شما از {path} به {user} انجام نشد.",
"The ownership transfer of {path} from {user} failed." : "انتقال مالکیت {path} از {user} انجام نشد.",
"Ownership transfer done" : "انتقال مالکیت انجام شد",
"Your ownership transfer of {path} to {user} has completed." : "انتقال مالکیت شما از {path} به {user} تکمیل شد.",
"The ownership transfer of {path} from {user} has completed." : "انتقال مالکیت {path} از {user} تکمیل شد.",
"in %s" : "در %s",
"File Management" : "مدیریت فایل",
"Reload current directory" : "دایرکتوری فعلی را دوباره بارگیری کنید",
"Go to the \"{dir}\" directory" : "به دایرکتوری \"{dir}\" بروید",
"Select the row for {displayName}" : "ردیف {displayName} را انتخاب کنید",
"Rename file" : "تغییر نام فایل",
"File name" : "نام فایل",
"A long time ago" : "مدت ها پیش",
"Download file {name}" : "دانلود فایل {name}",
"\"{displayName}\" action executed successfully" : "عملکرد \"{displayName}\" با موفقیت اجرا شد",
"\"{displayName}\" action failed" : "اقدام \"{displayName}\" ناموفق بود",
"\"{name}\" is not an allowed filetype." : "\"{name}\" یک نوع فایل مجاز نیست.",
"{newName} already exists." : "{newName} از قبل وجود دارد.",
"Name cannot be empty" : "نام نمی‌تواند خالی باشد",
"Another entry with the same name already exists" : "ورودی دیگری با همین نام در حال حاضر وجود دارد",
"Renamed \"{oldName}\" to \"{newName}\"" : "تغییر نام \"{oldName}\" به \"{newName}\"",
"Could not rename \"{oldName}\", it does not exist any more" : "نمی‌توان نام «{oldName}» را تغییر داد، دیگر وجود ندارد",
"The name \"{newName}\"\" is already used in the folder \"{dir}\". Please choose a different name." : "نام \"{newName}\" قبلاً در پوشه \"{dir}\" استفاده شده است. لطفاً نام دیگری انتخاب کنید.",
"Could not rename \"{oldName}\"" : "تغییر نام \"{oldName}\" ممکن نیست",
"Total rows summary" : "خلاصه کل ردیف ها",
"Select all" : "انتخاب همه",
"Unselect all" : "همه را لغو انتخاب کنید",
"\"{displayName}\" failed on some elements " : "\"{displayName}\" در برخی از عناصر ناموفق بود",
"\"{displayName}\" batch action executed successfully" : "عملکرد دسته‌ای \"{displayName}\" با موفقیت اجرا شد",
"ascending" : "صعودی",
"descending" : "نزولی",
"Sort list by {column} ({direction})" : "مرتب سازی لیست بر اساس {ستون} ({direction})",
"List of files and folders." : "لیست فایل ها و پوشه ها",
"This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "این لیست به دلایل عملکرد به طور کامل ارائه نشده است. در حین حرکت در لیست، فایل ها ارائه می شوند.",
"Storage informations" : "ذخیره سازی اطلاعات ",
"{usedQuotaByte} used" : "{usedQuotaByte} استفاده شده است",
"{relative}% used" : "{نسبی}% استفاده شده است",
"Could not refresh storage stats" : "نمی‌توان آمار ذخیره‌سازی را بازخوانی کرد",
"Transfer ownership of a file or folder" : "انتقال مالکیت یک فایل یا پوشه",
"Choose file or folder to transfer" : "فایل یا پوشه را برای انتقال انتخاب کنید",
"Change" : "تغییر",
"New owner" : "مالک جدید",
"Choose a file or folder to transfer" : "فایل یا پوشه ای را برای انتقال انتخاب کنید",
"Transfer" : "انتقال",
"Transfer {path} to {userid}" : "انتقال {path} به {userid}",
"Invalid path selected" : "مسیر نامعتبر انتخاب شده است",
"Unknown error" : "خطای نامشخص",
"Ownership transfer request sent" : "درخواست انتقال مالکیت ارسال شد",
"Cannot transfer ownership of a file or folder you do not own" : "نمی‌توان مالکیت فایل یا پوشه‌ای را که متعلق به شما نیست، منتقل کرد",
"Select file or folder to link to" : "فایل یا پوشه را برای پیوند انتخاب کنید",
"Loading current folder" : "در حال بارگیری پوشه فعلی",
"No files in here" : "هیچ فایلی اینجا وجود ندارد",
"Upload some content or sync with your devices!" : "محتوایی را آپلود کنید یا با دستگاه خود همگام‌سازی کنید!",
"Go to the previous folder" : "به پوشه قبلی بروید",
"Go back" : "برگرد",
"Open the files app settings" : "تنظیمات برنامه فایل ها را باز کنید",
"Files settings" : "تنظیمات پرونده‌ها",
"File cannot be accessed" : "فایل قابل دسترسی نیست",
"You might not have have permissions to view it, ask the sender to share it" : "ممکن است مجوز مشاهده آن را نداشته باشید، از فرستنده بخواهید آن را به اشتراک بگذارد",
"Sort favorites first" : "ابتدا موارد دلخواه را مرتب کنید",
"Show hidden files" : "نمایش پرونده‌های مخفی",
"Crop image previews" : "پیش نمایش تصویر برش",
"Additional settings" : "تنظیمات اضافی",
"WebDAV" : "WebDAV",
"Copy to clipboard" : "رونوشت به تخته‌گیره",
"Use this address to access your Files via WebDAV" : "از این آدرس برای دسترسی به فایل های خود از طریق WebDAV استفاده کنید",
"If you have enabled 2FA, you must create and use a new app password by clicking here." : "اگر 2FA را فعال کرده اید، باید با کلیک کردن در اینجا یک رمز عبور برنامه جدید ایجاد و استفاده کنید.",
"Clipboard is not available" : "تخته گیره موحود نیست",
"WebDAV URL copied to clipboard" : "URL WebDAV در کلیپ بورد کپی شد",
"Unable to change the favourite state of the file" : "امکان تغییر حالت دلخواه فایل وجود ندارد",
"Error while loading the file data" : "خطا هنگام بارگیری داده های فایل",
"Pick a template for {name}" : "یک الگو برای {name} انتخاب کنید",
"Create" : "ساخت",
"Create a new file with the selected template" : "یک فایل جدید با الگوی انتخاب شده ایجاد کنید",
"Creating file" : "ایجاد فایل",
"Blank" : "جای خالی",
"Unable to create new file from template" : "امکان ایجاد فایل جدید از الگو وجود ندارد",
"Delete permanently" : "حذف قطعی",
"Open folder {displayName}" : "باز کردن پوشه {displayName}",
"Open in Files" : "در فایل باز کنید",
"Open details" : "باز کردن جزئیات",
"Set up templates folder" : "پوشه قالب ها را تنظیم کنید",
"Templates" : "قالب‌ها",
"Create new templates folder" : "پوشه قالب های جدید ایجاد کنید",
"Unable to initialize the templates directory" : "راه اندازی دایرکتوری الگوها ممکن نیست",
"List of favorites files and folders." : "لیست فایل ها و پوشه های مورد علاقه",
"No favorites yet" : "هنوز مورد دلخواه وجود ندارد",
"Files and folders you mark as favorite will show up here" : "فایل‌ها و پوشه‌های انتخاب شده به عنوان برگزیده توسط شما، در اینجا نمایش داده می‌شود",
"List of recently modified files and folders." : "فهرست فایل‌ها و پوشه‌هایی که اخیراً اصلاح شده‌اند.",
"No recently modified files" : "هیچ فایلی که اخیراً اصلاح شده است",
"Files and folders you recently modified will show up here." : "فایل‌ها و پوشه‌هایی که اخیراً تغییر داده‌اید در اینجا نمایش داده می‌شوند.",
"Toggle %1$s sublist" : "تغییر%1$s فهرست فرعی",
"Toggle grid view" : "نمای شبکه را تغییر دهید",
"No entries found in this folder" : "هیچ ورودی‌ای در این پوشه وجود ندارد",
"Upload too large" : "سایز فایل برای آپلود زیاد است(م.تنظیمات در php.ini)",
@ -126,8 +258,12 @@
"Text file" : "فایل متنی",
"New text file.txt" : "پروندهٔ متنی جدید با پسوند txt",
"Storage invalid" : "فضای ذخیره‌سازی نامعتبر",
"You can only favorite a single file or folder at a time" : "در هر زمان فقط می توانید یک فایل یا پوشه را مورد علاقه خود قرار دهید",
"Unlimited" : "نامحدود",
"Search users" : "جستجوی کاربران",
"Cancel" : "لغو",
"%s used" : "%sاستفاده شده",
"%s%%" : "%s%%",
"%1$s of %2$s used" : "%1$s از %2$s استفاده شده ",
"Deleted files" : "پرونده‌های حذف شده",
"Shares" : "اشتراک گذاری ها",
@ -135,6 +271,10 @@
"Shared with you" : "Shared with you",
"Shared by link" : "اشتراک گذاشته شده از طریق لینک",
"Deleted shares" : "اشتراک گذاری های حذف شده",
"Pending shares" : "اشتراک در حال انتظار "
"Pending shares" : "اشتراک در حال انتظار ",
"Open folder {name}" : "باز کردن پوشه {name}",
"This list is not fully rendered for performances reasons. The files will be rendered as you navigate through the list." : "این لیست به دلایل اجرایی به طور کامل ارائه نشده است. در حین حرکت در لیست، فایل ها ارائه می شوند.",
"Search for an account" : "جستجو برای یک حساب کاربری",
"No files or folders have been deleted yet" : "هنوز هیچ فایل یا پوشه ای حذف نشده است"
},"pluralForm" :"nplurals=2; plural=(n > 1);"
}

View file

@ -32,6 +32,7 @@ OC.L10N.register(
"Move" : "Siirrä",
"Copy" : "Kopioi",
"Choose target folder" : "Valitse kohdekansio",
"Set reminder" : "Aseta muistutus",
"Edit locally" : "Muokkaa paikallisesti",
"Open" : "Avaa",
"Delete file" : "Poista tiedosto",
@ -179,6 +180,7 @@ OC.L10N.register(
"Name cannot be empty" : "Nimi ei voi olla tyhjä",
"Another entry with the same name already exists" : "Toinen tietue samalla nimellä on jo olemassa",
"Renamed \"{oldName}\" to \"{newName}\"" : "Kohteen \"{oldName}\" uudeksi nimeksi asetettiin \"{newName}\"",
"Could not rename \"{oldName}\", it does not exist any more" : "Kohdetta \"{oldName}\" ei voitu nimetä uudelleen, koska sitä ei ole enää olemassa",
"Could not rename \"{oldName}\"" : "Ei voitu nimetä uudelleen \"{oldName}\"",
"Select all" : "Valitse kaikki",
"Unselect all" : "Poista valinnat",
@ -186,6 +188,7 @@ OC.L10N.register(
"descending" : "laskevasti",
"Sort list by {column} ({direction})" : "Järjestä luettelo sarakkeen {column} mukaan ({direction})",
"List of files and folders." : "Luettelo tiedostoista ja kansioista.",
"This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Tätä luetteloa ei ole esitetty täysin suorituskykyyn liittyvistä syistä. Tiedostot esitetään sitä mukaa, kun selaat luetteloa.",
"Storage informations" : "Tallennustilan tietoja",
"{usedQuotaByte} used" : "{usedQuotaByte} käytetty",
"{relative}% used" : "{relative} % käytetty",
@ -239,6 +242,9 @@ OC.L10N.register(
"List of favorites files and folders." : "Luettelo suosikkitiedostoista ja -kansioista.",
"No favorites yet" : "Ei vielä suosikkeja",
"Files and folders you mark as favorite will show up here" : "Suosikeiksi merkitsemäsi tiedostot ja kansiot näkyvät täällä",
"List of recently modified files and folders." : "Luettelo äskettäin muokatuista tiedostoista ja kansioista.",
"No recently modified files" : "Ei äskettäin muokattuja tiedostoja",
"Files and folders you recently modified will show up here." : "Äskettäin muokkaamasi tiedostot ja kansiot näkyvät täällä.",
"Toggle grid view" : "Ruudukkonäkymä päälle/pois",
"No entries found in this folder" : "Ei kohteita tässä kansiossa",
"Upload too large" : "Lähetettävä tiedosto on liian suuri",

View file

@ -30,6 +30,7 @@
"Move" : "Siirrä",
"Copy" : "Kopioi",
"Choose target folder" : "Valitse kohdekansio",
"Set reminder" : "Aseta muistutus",
"Edit locally" : "Muokkaa paikallisesti",
"Open" : "Avaa",
"Delete file" : "Poista tiedosto",
@ -177,6 +178,7 @@
"Name cannot be empty" : "Nimi ei voi olla tyhjä",
"Another entry with the same name already exists" : "Toinen tietue samalla nimellä on jo olemassa",
"Renamed \"{oldName}\" to \"{newName}\"" : "Kohteen \"{oldName}\" uudeksi nimeksi asetettiin \"{newName}\"",
"Could not rename \"{oldName}\", it does not exist any more" : "Kohdetta \"{oldName}\" ei voitu nimetä uudelleen, koska sitä ei ole enää olemassa",
"Could not rename \"{oldName}\"" : "Ei voitu nimetä uudelleen \"{oldName}\"",
"Select all" : "Valitse kaikki",
"Unselect all" : "Poista valinnat",
@ -184,6 +186,7 @@
"descending" : "laskevasti",
"Sort list by {column} ({direction})" : "Järjestä luettelo sarakkeen {column} mukaan ({direction})",
"List of files and folders." : "Luettelo tiedostoista ja kansioista.",
"This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Tätä luetteloa ei ole esitetty täysin suorituskykyyn liittyvistä syistä. Tiedostot esitetään sitä mukaa, kun selaat luetteloa.",
"Storage informations" : "Tallennustilan tietoja",
"{usedQuotaByte} used" : "{usedQuotaByte} käytetty",
"{relative}% used" : "{relative} % käytetty",
@ -237,6 +240,9 @@
"List of favorites files and folders." : "Luettelo suosikkitiedostoista ja -kansioista.",
"No favorites yet" : "Ei vielä suosikkeja",
"Files and folders you mark as favorite will show up here" : "Suosikeiksi merkitsemäsi tiedostot ja kansiot näkyvät täällä",
"List of recently modified files and folders." : "Luettelo äskettäin muokatuista tiedostoista ja kansioista.",
"No recently modified files" : "Ei äskettäin muokattuja tiedostoja",
"Files and folders you recently modified will show up here." : "Äskettäin muokkaamasi tiedostot ja kansiot näkyvät täällä.",
"Toggle grid view" : "Ruudukkonäkymä päälle/pois",
"No entries found in this folder" : "Ei kohteita tässä kansiossa",
"Upload too large" : "Lähetettävä tiedosto on liian suuri",

View file

@ -194,6 +194,7 @@ OC.L10N.register(
"descending" : "decrescente",
"Sort list by {column} ({direction})" : "Ordina la lista per {column} ({direction})",
"List of files and folders." : "Lista di file e cartelle.",
"This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Questa lista non è stata mostrata completamente per ragioni di prestazioni. I file verranno mostrati durante la navigazione della lista.",
"Storage informations" : "Informazioni di archiviazione",
"{usedQuotaByte} used" : "{usedQuotaByte} usato",
"{relative}% used" : "{relative}% usato",
@ -239,6 +240,7 @@ OC.L10N.register(
"Unable to create new file from template" : "Impossibile creare un nuovo file dal modello",
"Delete permanently" : "Elimina permanentemente",
"Open folder {displayName}" : "Apri la cartella {displayName}",
"Open in Files" : "Apri in File",
"Open details" : "Apri i dettagli",
"Set up templates folder" : "Configura la cartella dei modelli",
"Templates" : "Modelli",
@ -247,6 +249,9 @@ OC.L10N.register(
"List of favorites files and folders." : "Lista di file e cartelle preferiti.",
"No favorites yet" : "Nessun preferito ancora",
"Files and folders you mark as favorite will show up here" : "I file e le cartelle che marchi come preferiti saranno mostrati qui",
"List of recently modified files and folders." : "Lista di file e cartelle modificati di recente.",
"No recently modified files" : "Nessun file modificato di recente",
"Files and folders you recently modified will show up here." : "I file e le cartelle che hai modificato di recente saranno mostrati qui.",
"Toggle %1$s sublist" : "Passa alla sottolista %1$s",
"Toggle grid view" : "Commuta la vista a griglia",
"No entries found in this folder" : "Nessuna voce trovata in questa cartella",

View file

@ -192,6 +192,7 @@
"descending" : "decrescente",
"Sort list by {column} ({direction})" : "Ordina la lista per {column} ({direction})",
"List of files and folders." : "Lista di file e cartelle.",
"This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Questa lista non è stata mostrata completamente per ragioni di prestazioni. I file verranno mostrati durante la navigazione della lista.",
"Storage informations" : "Informazioni di archiviazione",
"{usedQuotaByte} used" : "{usedQuotaByte} usato",
"{relative}% used" : "{relative}% usato",
@ -237,6 +238,7 @@
"Unable to create new file from template" : "Impossibile creare un nuovo file dal modello",
"Delete permanently" : "Elimina permanentemente",
"Open folder {displayName}" : "Apri la cartella {displayName}",
"Open in Files" : "Apri in File",
"Open details" : "Apri i dettagli",
"Set up templates folder" : "Configura la cartella dei modelli",
"Templates" : "Modelli",
@ -245,6 +247,9 @@
"List of favorites files and folders." : "Lista di file e cartelle preferiti.",
"No favorites yet" : "Nessun preferito ancora",
"Files and folders you mark as favorite will show up here" : "I file e le cartelle che marchi come preferiti saranno mostrati qui",
"List of recently modified files and folders." : "Lista di file e cartelle modificati di recente.",
"No recently modified files" : "Nessun file modificato di recente",
"Files and folders you recently modified will show up here." : "I file e le cartelle che hai modificato di recente saranno mostrati qui.",
"Toggle %1$s sublist" : "Passa alla sottolista %1$s",
"Toggle grid view" : "Commuta la vista a griglia",
"No entries found in this folder" : "Nessuna voce trovata in questa cartella",

View file

@ -193,6 +193,8 @@ OC.L10N.register(
"ascending" : "за зростанням",
"descending" : "за спаданням",
"Sort list by {column} ({direction})" : "Впорядкувати список за {column} ({direction})",
"List of files and folders." : "Список файлів та каталогів",
"This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Список не подається повністю з міркувань обчислювальних потужностей. Файли показуватимуться під час прокручування списку.",
"Storage informations" : "Інформація про сховище",
"{usedQuotaByte} used" : "{usedQuotaByte} використано",
"{relative}% used" : "{relative}% використано",
@ -238,6 +240,7 @@ OC.L10N.register(
"Unable to create new file from template" : "Неможливо створити новий файл з шаблону",
"Delete permanently" : "Вилучити назавжди",
"Open folder {displayName}" : "Відкрити каталог {displayName}",
"Open in Files" : "Відкрити у Файлах",
"Open details" : "Показати деталі",
"Set up templates folder" : "Встановити каталог з шаблонами",
"Templates" : "Шаблони",
@ -246,6 +249,9 @@ OC.L10N.register(
"List of favorites files and folders." : "Список вподобаних файлів та каталогів.",
"No favorites yet" : "Поки немає вподобаного",
"Files and folders you mark as favorite will show up here" : "Файли та каталоги, які ви вподобали, з’являться тут",
"List of recently modified files and folders." : "Список нещодавно змінених файлів та каталогів.",
"No recently modified files" : "Відсутні файли із нещодавними змінами",
"Files and folders you recently modified will show up here." : "Тут показуватимуться файли та каталоги, які було нещодавно змінено.",
"Toggle %1$s sublist" : "Перемкнути вкладений список %1$s",
"Toggle grid view" : "Перемкнути подання сіткою",
"No entries found in this folder" : "В цьому каталозі нічого не знайдено",

View file

@ -191,6 +191,8 @@
"ascending" : "за зростанням",
"descending" : "за спаданням",
"Sort list by {column} ({direction})" : "Впорядкувати список за {column} ({direction})",
"List of files and folders." : "Список файлів та каталогів",
"This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Список не подається повністю з міркувань обчислювальних потужностей. Файли показуватимуться під час прокручування списку.",
"Storage informations" : "Інформація про сховище",
"{usedQuotaByte} used" : "{usedQuotaByte} використано",
"{relative}% used" : "{relative}% використано",
@ -236,6 +238,7 @@
"Unable to create new file from template" : "Неможливо створити новий файл з шаблону",
"Delete permanently" : "Вилучити назавжди",
"Open folder {displayName}" : "Відкрити каталог {displayName}",
"Open in Files" : "Відкрити у Файлах",
"Open details" : "Показати деталі",
"Set up templates folder" : "Встановити каталог з шаблонами",
"Templates" : "Шаблони",
@ -244,6 +247,9 @@
"List of favorites files and folders." : "Список вподобаних файлів та каталогів.",
"No favorites yet" : "Поки немає вподобаного",
"Files and folders you mark as favorite will show up here" : "Файли та каталоги, які ви вподобали, з’являться тут",
"List of recently modified files and folders." : "Список нещодавно змінених файлів та каталогів.",
"No recently modified files" : "Відсутні файли із нещодавними змінами",
"Files and folders you recently modified will show up here." : "Тут показуватимуться файли та каталоги, які було нещодавно змінено.",
"Toggle %1$s sublist" : "Перемкнути вкладений список %1$s",
"Toggle grid view" : "Перемкнути подання сіткою",
"No entries found in this folder" : "В цьому каталозі нічого не знайдено",

View file

@ -6,6 +6,8 @@ OC.L10N.register(
"Download" : "Tải về",
"Delete" : "Xóa",
"Tags" : "Nhãn",
"Show list view" : "Hiển thị chế độ xem danh sách",
"Show grid view" : "Hiển thị chế độ xem lưới",
"Home" : "Nhà",
"Close" : "Đóng",
"Could not create folder \"{dir}\"" : "Không thể tạo thư mục “{dir}”",
@ -18,6 +20,7 @@ OC.L10N.register(
"Target folder \"{dir}\" does not exist any more" : "Thư mục đích \"{dir}\" không còn tồn tại",
"Not enough free space" : "Không đủ dung lượng trống",
"An unknown error has occurred" : "Một lỗi không rõ đã xảy ra",
"File could not be uploaded" : "Không thể tải lên tập tin",
"Uploading …" : "Đang tải lên …",
"{loadedSize} of {totalSize} ({bitrate})" : "{loadedSize} trong tổng số {totalSize} ({bitrate})",
"Uploading that item is not supported" : "Tải lên mục ‎‎đó không được hỗ trợ",
@ -25,10 +28,11 @@ OC.L10N.register(
"Operation is blocked by access control" : "Thao tác bị chặn bởi kiểm soát truy cập",
"Error when assembling chunks, status code {status}" : "Lỗi khi lắp ráp khối, mã trạng thái {status}",
"Actions" : "Actions",
"Rename" : "Sửa tên",
"Rename" : "Đổi tên",
"Move" : "Dịch chuyển",
"Copy" : "Sao chép",
"Choose target folder" : "Chọn thư mục đích",
"Edit locally" : "Chỉnh sửa cục bộ/ngoại tuyến",
"Open" : "Mở",
"Delete file" : "Xóa tệp",
"Delete folder" : "Xóa thư mục",
@ -39,6 +43,8 @@ OC.L10N.register(
"Details" : "Chi tiết",
"Please select tag(s) to add to the selection" : "Vui lòng chọn (các) thẻ để thêm vào lựa chọn",
"Apply tag(s) to selection" : "Áp dụng (các) thẻ cho lựa chọn",
"Select directory \"{dirName}\"" : "Chọn thư mục \"{dirName}\"",
"Select file \"{fileName}\"" : "Chọn tệp tin \"{fileName}\"",
"Pending" : "Đang chờ",
"Unable to determine date" : "Không thể xác định ngày",
"This operation is forbidden" : "Thao tác bị cấm",
@ -51,6 +57,7 @@ OC.L10N.register(
"Could not copy \"{file}\"" : "Không thể sao chép tệp tin \"{file}\"",
"Copied {origin} inside {destination}" : "Được sao chép {origin} vào trong {destination}",
"Copied {origin} and {nbfiles} other files inside {destination}" : "Đã sao chép {origin} và {nbfiles} các file vào trong {destination}",
"Failed to redirect to client" : "Không thể chuyển hướng đến ứng dụng khách",
"{newName} already exists" : "{newName} đã có",
"Could not rename \"{fileName}\", it does not exist any more" : "Không thể đổi tên \"{fileName}\", tập tin không tồn tại",
"The name \"{targetName}\" is already used in the folder \"{dir}\". Please choose a different name." : "Tên \"{targetName}\" đã được dùng trong thư mục \"{dir}\". Hãy thử tên khác",
@ -69,7 +76,9 @@ OC.L10N.register(
"_%n file_::_%n files_" : ["%n tập tin"],
"{dirs} and {files}" : "{dirs} và {files}",
"_including %n hidden_::_including %n hidden_" : ["Bao gồm %n ẩn"],
"You do not have permission to upload or create files here" : "Bạn không đủ quyền để Tải lên hoặc Tạo tập tin ở đây",
"_Uploading %n file_::_Uploading %n files_" : ["Đang tải lên %n tập tin"],
"New file/folder menu" : "Menu tệp tin/thư mục mới",
"Select file range" : "Chọn phạm vi tệp",
"{used} of {quota} used" : "{used} trong { quota } được sử dụng",
"{used} used" : "{used} được sử dụng",
@ -87,6 +96,7 @@ OC.L10N.register(
"Your storage is almost full ({usedSpacePercent}%)." : "Dung lượng lưu trữ của bạn gần đầy rồi ({usedSpacePercent}%).",
"_matches \"{filter}\"_::_match \"{filter}\"_" : ["khớp với \"{filter}\""],
"View in folder" : "Xem trong thư mục",
"Direct link was copied (only works for users who have access to this file/folder)" : "Đã sao chép đường dẫn trực tiếp(Chỉ áp dụng đối với người dùng có quyền truy cập vào thư mục/tệp tin)",
"Path" : "Đường dẫn",
"_%n byte_::_%n bytes_" : ["%n bytes"],
"Favorited" : "Được ưa thích",
@ -96,6 +106,8 @@ OC.L10N.register(
"Create new folder" : "Tạo thư mục mới",
"Upload file" : "Tải lên tập tin",
"Recent" : "Gần đây",
"This file has the tag {tag}" : "Tập tin này có nhãn {tag}",
"This file has the tags {firstTags} and {lastTag}" : "Tệp tin này có nhãn {firstTags} và {lastTag}",
"Not favorited" : "Không được yêu thích",
"Remove from favorites" : "Xóa khỏi ưa thích",
"Add to favorites" : "Thêm vào ưa thích",
@ -155,7 +167,21 @@ OC.L10N.register(
"The ownership transfer of {path} from {user} has completed." : "Việc chuyển quyền sở hữu {path} từ {user} đã hoàn tất.",
"in %s" : "trong %s",
"File Management" : "Quản lý tệp tin",
"Reload current directory" : "Tải lại thư mục hiện tại",
"Go to the \"{dir}\" directory" : "Đi đến thư mục \"{dir}\"",
"Select the row for {displayName}" : "Chọn hàng cho {displayName}",
"Rename file" : "Đổi tên tệp tin",
"File name" : "Tên tệp tin",
"A long time ago" : "Một khoảng thời gian trước",
"Download file {name}" : "Tải xuống tệp tin {name}",
"\"{displayName}\" action executed successfully" : "Hành động \"{displayName}\" đã thực thi thành công",
"\"{displayName}\" action failed" : "Hành động \"{displayName}\" thất bại",
"\"{name}\" is not an allowed filetype." : "\"{name}\" không phải là định dạng được cho phép",
"{newName} already exists." : "{newName} đã tồn tại.",
"Name cannot be empty" : "Tên không thể trống",
"Another entry with the same name already exists" : "Đã tồn tại mục cùng tên",
"Renamed \"{oldName}\" to \"{newName}\"" : "Đã đổi tên \"{oldName}\" thành \"{newName}\"",
"Could not rename \"{oldName}\", it does not exist any more" : "Không thể đổi tên \"{oldName}\", tệp tin không còn tồn tại",
"Select all" : "Chọn tất cả",
"Transfer ownership of a file or folder" : "Chuyển quyền sở hữu tệp hoặc thư mục",
"Choose file or folder to transfer" : "Chọn tệp hoặc cặp để chuyển",

View file

@ -4,6 +4,8 @@
"Download" : "Tải về",
"Delete" : "Xóa",
"Tags" : "Nhãn",
"Show list view" : "Hiển thị chế độ xem danh sách",
"Show grid view" : "Hiển thị chế độ xem lưới",
"Home" : "Nhà",
"Close" : "Đóng",
"Could not create folder \"{dir}\"" : "Không thể tạo thư mục “{dir}”",
@ -16,6 +18,7 @@
"Target folder \"{dir}\" does not exist any more" : "Thư mục đích \"{dir}\" không còn tồn tại",
"Not enough free space" : "Không đủ dung lượng trống",
"An unknown error has occurred" : "Một lỗi không rõ đã xảy ra",
"File could not be uploaded" : "Không thể tải lên tập tin",
"Uploading …" : "Đang tải lên …",
"{loadedSize} of {totalSize} ({bitrate})" : "{loadedSize} trong tổng số {totalSize} ({bitrate})",
"Uploading that item is not supported" : "Tải lên mục ‎‎đó không được hỗ trợ",
@ -23,10 +26,11 @@
"Operation is blocked by access control" : "Thao tác bị chặn bởi kiểm soát truy cập",
"Error when assembling chunks, status code {status}" : "Lỗi khi lắp ráp khối, mã trạng thái {status}",
"Actions" : "Actions",
"Rename" : "Sửa tên",
"Rename" : "Đổi tên",
"Move" : "Dịch chuyển",
"Copy" : "Sao chép",
"Choose target folder" : "Chọn thư mục đích",
"Edit locally" : "Chỉnh sửa cục bộ/ngoại tuyến",
"Open" : "Mở",
"Delete file" : "Xóa tệp",
"Delete folder" : "Xóa thư mục",
@ -37,6 +41,8 @@
"Details" : "Chi tiết",
"Please select tag(s) to add to the selection" : "Vui lòng chọn (các) thẻ để thêm vào lựa chọn",
"Apply tag(s) to selection" : "Áp dụng (các) thẻ cho lựa chọn",
"Select directory \"{dirName}\"" : "Chọn thư mục \"{dirName}\"",
"Select file \"{fileName}\"" : "Chọn tệp tin \"{fileName}\"",
"Pending" : "Đang chờ",
"Unable to determine date" : "Không thể xác định ngày",
"This operation is forbidden" : "Thao tác bị cấm",
@ -49,6 +55,7 @@
"Could not copy \"{file}\"" : "Không thể sao chép tệp tin \"{file}\"",
"Copied {origin} inside {destination}" : "Được sao chép {origin} vào trong {destination}",
"Copied {origin} and {nbfiles} other files inside {destination}" : "Đã sao chép {origin} và {nbfiles} các file vào trong {destination}",
"Failed to redirect to client" : "Không thể chuyển hướng đến ứng dụng khách",
"{newName} already exists" : "{newName} đã có",
"Could not rename \"{fileName}\", it does not exist any more" : "Không thể đổi tên \"{fileName}\", tập tin không tồn tại",
"The name \"{targetName}\" is already used in the folder \"{dir}\". Please choose a different name." : "Tên \"{targetName}\" đã được dùng trong thư mục \"{dir}\". Hãy thử tên khác",
@ -67,7 +74,9 @@
"_%n file_::_%n files_" : ["%n tập tin"],
"{dirs} and {files}" : "{dirs} và {files}",
"_including %n hidden_::_including %n hidden_" : ["Bao gồm %n ẩn"],
"You do not have permission to upload or create files here" : "Bạn không đủ quyền để Tải lên hoặc Tạo tập tin ở đây",
"_Uploading %n file_::_Uploading %n files_" : ["Đang tải lên %n tập tin"],
"New file/folder menu" : "Menu tệp tin/thư mục mới",
"Select file range" : "Chọn phạm vi tệp",
"{used} of {quota} used" : "{used} trong { quota } được sử dụng",
"{used} used" : "{used} được sử dụng",
@ -85,6 +94,7 @@
"Your storage is almost full ({usedSpacePercent}%)." : "Dung lượng lưu trữ của bạn gần đầy rồi ({usedSpacePercent}%).",
"_matches \"{filter}\"_::_match \"{filter}\"_" : ["khớp với \"{filter}\""],
"View in folder" : "Xem trong thư mục",
"Direct link was copied (only works for users who have access to this file/folder)" : "Đã sao chép đường dẫn trực tiếp(Chỉ áp dụng đối với người dùng có quyền truy cập vào thư mục/tệp tin)",
"Path" : "Đường dẫn",
"_%n byte_::_%n bytes_" : ["%n bytes"],
"Favorited" : "Được ưa thích",
@ -94,6 +104,8 @@
"Create new folder" : "Tạo thư mục mới",
"Upload file" : "Tải lên tập tin",
"Recent" : "Gần đây",
"This file has the tag {tag}" : "Tập tin này có nhãn {tag}",
"This file has the tags {firstTags} and {lastTag}" : "Tệp tin này có nhãn {firstTags} và {lastTag}",
"Not favorited" : "Không được yêu thích",
"Remove from favorites" : "Xóa khỏi ưa thích",
"Add to favorites" : "Thêm vào ưa thích",
@ -153,7 +165,21 @@
"The ownership transfer of {path} from {user} has completed." : "Việc chuyển quyền sở hữu {path} từ {user} đã hoàn tất.",
"in %s" : "trong %s",
"File Management" : "Quản lý tệp tin",
"Reload current directory" : "Tải lại thư mục hiện tại",
"Go to the \"{dir}\" directory" : "Đi đến thư mục \"{dir}\"",
"Select the row for {displayName}" : "Chọn hàng cho {displayName}",
"Rename file" : "Đổi tên tệp tin",
"File name" : "Tên tệp tin",
"A long time ago" : "Một khoảng thời gian trước",
"Download file {name}" : "Tải xuống tệp tin {name}",
"\"{displayName}\" action executed successfully" : "Hành động \"{displayName}\" đã thực thi thành công",
"\"{displayName}\" action failed" : "Hành động \"{displayName}\" thất bại",
"\"{name}\" is not an allowed filetype." : "\"{name}\" không phải là định dạng được cho phép",
"{newName} already exists." : "{newName} đã tồn tại.",
"Name cannot be empty" : "Tên không thể trống",
"Another entry with the same name already exists" : "Đã tồn tại mục cùng tên",
"Renamed \"{oldName}\" to \"{newName}\"" : "Đã đổi tên \"{oldName}\" thành \"{newName}\"",
"Could not rename \"{oldName}\", it does not exist any more" : "Không thể đổi tên \"{oldName}\", tệp tin không còn tồn tại",
"Select all" : "Chọn tất cả",
"Transfer ownership of a file or folder" : "Chuyển quyền sở hữu tệp hoặc thư mục",
"Choose file or folder to transfer" : "Chọn tệp hoặc cặp để chuyển",

View file

@ -32,6 +32,7 @@ OC.L10N.register(
"Move" : "移動",
"Copy" : "複製",
"Choose target folder" : "選擇目標資料夾",
"Set reminder" : "設定提醒",
"Edit locally" : "在近端編輯",
"Open" : "開啟",
"Delete file" : "刪除檔案",

View file

@ -30,6 +30,7 @@
"Move" : "移動",
"Copy" : "複製",
"Choose target folder" : "選擇目標資料夾",
"Set reminder" : "設定提醒",
"Edit locally" : "在近端編輯",
"Open" : "開啟",
"Delete file" : "刪除檔案",

View file

@ -32,6 +32,7 @@ OC.L10N.register(
"Move" : "移動",
"Copy" : "複製",
"Choose target folder" : "選擇目標資料夾",
"Set reminder" : "設定提醒",
"Edit locally" : "本機編輯",
"Open" : "開啟",
"Delete file" : "刪除檔案",

View file

@ -30,6 +30,7 @@
"Move" : "移動",
"Copy" : "複製",
"Choose target folder" : "選擇目標資料夾",
"Set reminder" : "設定提醒",
"Edit locally" : "本機編輯",
"Open" : "開啟",
"Delete file" : "刪除檔案",

View file

@ -104,8 +104,19 @@ OC.L10N.register(
"External storage support" : "Supporto archiviazioni esterne",
"Adds basic external storage support" : "Aggiunge un supporto di base per archiviazioni esterne",
"This application enables administrators to configure connections to external storage providers, such as FTP servers, S3 or SWIFT object stores, other Nextcloud servers, WebDAV servers, and more. Administrators can choose which types of storage to enable and can mount these storage locations for a user, a group, or the entire system. Users will see a new folder appear in their root Nextcloud directory, which they can access and use like any other Nextcloud folder. External storage also allows users to share files stored in these external locations. In these cases, the credentials for the owner of the file are used when the recipient requests the file from external storage, thereby ensuring that the recipient can access the shared file.\n\nExternal storage can be configured using the GUI or at the command line. This second option provides the advanced user with more flexibility for configuring bulk external storage mounts and setting mount priorities. More information is available in the external storage GUI documentation and the external storage Configuration File documentation." : "Questa applicazione consente agli amministratori di configurare connessioni a fornitori di archiviazione esterna, come server FTP, archivi di oggetti S3 o SWIFT, altri server Nextcloud, server WebDAV e altro. Gli amministratori possono scegliere quale tipo di archiviazione abilitare e possono montare queste posizioni di archiviazione per un utente, un gruppo o per l'intero sistema. Gli utenti vedranno una nuova cartella apparire nella loro cartella radice di Nextcloud, che possono accedere e utilizzare come qualsiasi altra cartella di Nextcloud. L'archiviazione esterna consente anche agli utenti di condividere file archiviati in queste posizioni esterne. In questi casi, le credenziali del proprietario del file sono utilizzate quando il destinatario richiede il file da archiviazione esterna, assicurando in tal modo che il destinatario possa accedere al file condiviso.\n\nL'archiviazione esterna può essere configurata utilizzando l'interfaccia grafica o la riga di comando. Questa seconda opzione fornisce maggiore flessibilità all'utente avanzato per una configurazione massiva dei punti di mount delle archiviazioni esterne e l'impostazione delle priorità dei punti di mount. Altre informazioni sono disponibili nella documentazione dell'interfaccia grafica dell'archiviazione esterna e nella documentazione del file di configurazione delle archiviazioni esterne.",
"Enter missing credentials" : "Digita le credenziali mancanti",
"Unable to update this external storage config. {statusMessage}" : "Impossibile aggiornare questa configurazione di archiviazione esterna. {statusMessage}",
"New configuration successfully saved" : "Nuova configurazione salvata correttamente",
"There was an error with this external storage." : "Si è verificato un errore con questa archiviazione esterna.",
"We were unable to check the external storage {basename}" : "Non è stato possibile controllare l'archiviazione esterna {basename}",
"Examine this faulty external storage configuration" : "Esamina questa configurazione errata di archiviazione esterna",
"Open in files" : "Apri in file",
"There was an error with this external storage. Do you want to review this mount point config in the settings page?" : "Si è verificato un errore con questa archiviazione esterna. Vuoi rivedere la configurazione del punto di mount nelle impostazioni?",
"External mount error" : "Errore di mount esterno",
"List of external storage." : "Lista di archiviazioni esterne.",
"There is no external storage configured. You can configure them in your Personal settings." : "Nessuna archiviazione esterna configurata. Puoi configurarla nelle impostazioni personali.",
"There is no external storage configured and you don't have the permission to configure them." : "Nessuna archiviazione esterna configurata e non hai i permessi per configurarla.",
"No external storage" : "Nessuna archiviazione esterna",
"Storage type" : "Tipo di archiviazione",
"Unknown" : "Sconosciuto",
"Scope" : "Ambito",

View file

@ -102,8 +102,19 @@
"External storage support" : "Supporto archiviazioni esterne",
"Adds basic external storage support" : "Aggiunge un supporto di base per archiviazioni esterne",
"This application enables administrators to configure connections to external storage providers, such as FTP servers, S3 or SWIFT object stores, other Nextcloud servers, WebDAV servers, and more. Administrators can choose which types of storage to enable and can mount these storage locations for a user, a group, or the entire system. Users will see a new folder appear in their root Nextcloud directory, which they can access and use like any other Nextcloud folder. External storage also allows users to share files stored in these external locations. In these cases, the credentials for the owner of the file are used when the recipient requests the file from external storage, thereby ensuring that the recipient can access the shared file.\n\nExternal storage can be configured using the GUI or at the command line. This second option provides the advanced user with more flexibility for configuring bulk external storage mounts and setting mount priorities. More information is available in the external storage GUI documentation and the external storage Configuration File documentation." : "Questa applicazione consente agli amministratori di configurare connessioni a fornitori di archiviazione esterna, come server FTP, archivi di oggetti S3 o SWIFT, altri server Nextcloud, server WebDAV e altro. Gli amministratori possono scegliere quale tipo di archiviazione abilitare e possono montare queste posizioni di archiviazione per un utente, un gruppo o per l'intero sistema. Gli utenti vedranno una nuova cartella apparire nella loro cartella radice di Nextcloud, che possono accedere e utilizzare come qualsiasi altra cartella di Nextcloud. L'archiviazione esterna consente anche agli utenti di condividere file archiviati in queste posizioni esterne. In questi casi, le credenziali del proprietario del file sono utilizzate quando il destinatario richiede il file da archiviazione esterna, assicurando in tal modo che il destinatario possa accedere al file condiviso.\n\nL'archiviazione esterna può essere configurata utilizzando l'interfaccia grafica o la riga di comando. Questa seconda opzione fornisce maggiore flessibilità all'utente avanzato per una configurazione massiva dei punti di mount delle archiviazioni esterne e l'impostazione delle priorità dei punti di mount. Altre informazioni sono disponibili nella documentazione dell'interfaccia grafica dell'archiviazione esterna e nella documentazione del file di configurazione delle archiviazioni esterne.",
"Enter missing credentials" : "Digita le credenziali mancanti",
"Unable to update this external storage config. {statusMessage}" : "Impossibile aggiornare questa configurazione di archiviazione esterna. {statusMessage}",
"New configuration successfully saved" : "Nuova configurazione salvata correttamente",
"There was an error with this external storage." : "Si è verificato un errore con questa archiviazione esterna.",
"We were unable to check the external storage {basename}" : "Non è stato possibile controllare l'archiviazione esterna {basename}",
"Examine this faulty external storage configuration" : "Esamina questa configurazione errata di archiviazione esterna",
"Open in files" : "Apri in file",
"There was an error with this external storage. Do you want to review this mount point config in the settings page?" : "Si è verificato un errore con questa archiviazione esterna. Vuoi rivedere la configurazione del punto di mount nelle impostazioni?",
"External mount error" : "Errore di mount esterno",
"List of external storage." : "Lista di archiviazioni esterne.",
"There is no external storage configured. You can configure them in your Personal settings." : "Nessuna archiviazione esterna configurata. Puoi configurarla nelle impostazioni personali.",
"There is no external storage configured and you don't have the permission to configure them." : "Nessuna archiviazione esterna configurata e non hai i permessi per configurarla.",
"No external storage" : "Nessuna archiviazione esterna",
"Storage type" : "Tipo di archiviazione",
"Unknown" : "Sconosciuto",
"Scope" : "Ambito",

View file

@ -17,7 +17,7 @@ OC.L10N.register(
"Are you sure you want to disconnect this external storage? It will make the storage unavailable in Nextcloud and will lead to a deletion of these files and folders on any sync client that is currently connected but will not delete any files and folders on the external storage itself." : "Ви впевнені, що бажаєте від’єднати цю зовнішню пам’ять? Це зробить сховище недоступним у Nextcloud і призведе до видалення цих файлів і папок на будь-якому клієнті синхронізації, який наразі підключено, але не вилучіть жодних файлів і папок на самому зовнішньому сховищі.",
"Delete storage?" : "Вилучити сховище?",
"Saved" : "Збережено",
"Saving …" : "Збереження ..",
"Saving …" : "Збереження ",
"Save" : "Зберегти",
"Forbidden to manage local mounts" : "Заборонено керувати місцевими кріпленнями",
"Storage with ID \"%d\" not found" : "Сховище з ідентифікатором \"%d\" не знайдено",

View file

@ -15,7 +15,7 @@
"Are you sure you want to disconnect this external storage? It will make the storage unavailable in Nextcloud and will lead to a deletion of these files and folders on any sync client that is currently connected but will not delete any files and folders on the external storage itself." : "Ви впевнені, що бажаєте від’єднати цю зовнішню пам’ять? Це зробить сховище недоступним у Nextcloud і призведе до видалення цих файлів і папок на будь-якому клієнті синхронізації, який наразі підключено, але не вилучіть жодних файлів і папок на самому зовнішньому сховищі.",
"Delete storage?" : "Вилучити сховище?",
"Saved" : "Збережено",
"Saving …" : "Збереження ..",
"Saving …" : "Збереження ",
"Save" : "Зберегти",
"Forbidden to manage local mounts" : "Заборонено керувати місцевими кріпленнями",
"Storage with ID \"%d\" not found" : "Сховище з ідентифікатором \"%d\" не знайдено",

View file

@ -0,0 +1,32 @@
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>files_reminders</id>
<name>File reminders</name>
<summary>Set file reminders</summary>
<description><![CDATA[**📣 File reminders**
Set file reminders.
]]></description>
<version>1.0.0</version>
<licence>agpl</licence>
<author>Christopher Ng</author>
<namespace>FilesReminders</namespace>
<category>files</category>
<bugs>https://github.com/nextcloud/server/issues</bugs>
<dependencies>
<nextcloud min-version="27" max-version="28" />
</dependencies>
<background-jobs>
<job>OCA\FilesReminders\BackgroundJob\CleanUpReminders</job>
<job>OCA\FilesReminders\BackgroundJob\ScheduledNotifications</job>
</background-jobs>
<commands>
<command>OCA\FilesReminders\Command\ListCommand</command>
</commands>
</info>

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
$requirements = [
'version' => '1',
];
return [
'ocs' => [
['name' => 'Api#get', 'url' => '/api/v{version}/get/{fileId}', 'verb' => 'GET', 'requirements' => $requirements],
['name' => 'Api#set', 'url' => '/api/v{version}/set/{fileId}', 'verb' => 'PUT', 'requirements' => $requirements],
['name' => 'Api#remove', 'url' => '/api/v{version}/remove/{fileId}', 'verb' => 'DELETE', 'requirements' => $requirements],
],
];

View file

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitFilesReminders::getLoader();

View file

@ -0,0 +1,13 @@
{
"config" : {
"vendor-dir": ".",
"optimize-autoloader": true,
"classmap-authoritative": true,
"autoloader-suffix": "FilesReminders"
},
"autoload" : {
"psr-4": {
"OCA\\FilesReminders\\": "../lib/"
}
}
}

View file

@ -0,0 +1,18 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d751713988987e9331980363e24189ce",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

View file

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View file

@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

View file

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,26 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = $vendorDir;
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OCA\\FilesReminders\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\FilesReminders\\BackgroundJob\\CleanUpReminders' => $baseDir . '/../lib/BackgroundJob/CleanUpReminders.php',
'OCA\\FilesReminders\\BackgroundJob\\ScheduledNotifications' => $baseDir . '/../lib/BackgroundJob/ScheduledNotifications.php',
'OCA\\FilesReminders\\Command\\ListCommand' => $baseDir . '/../lib/Command/ListCommand.php',
'OCA\\FilesReminders\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
'OCA\\FilesReminders\\Db\\Reminder' => $baseDir . '/../lib/Db/Reminder.php',
'OCA\\FilesReminders\\Db\\ReminderMapper' => $baseDir . '/../lib/Db/ReminderMapper.php',
'OCA\\FilesReminders\\Exception\\NodeNotFoundException' => $baseDir . '/../lib/Exception/NodeNotFoundException.php',
'OCA\\FilesReminders\\Exception\\UserNotFoundException' => $baseDir . '/../lib/Exception/UserNotFoundException.php',
'OCA\\FilesReminders\\Listener\\LoadAdditionalScriptsListener' => $baseDir . '/../lib/Listener/LoadAdditionalScriptsListener.php',
'OCA\\FilesReminders\\Listener\\NodeDeletedListener' => $baseDir . '/../lib/Listener/NodeDeletedListener.php',
'OCA\\FilesReminders\\Listener\\UserDeletedListener' => $baseDir . '/../lib/Listener/UserDeletedListener.php',
'OCA\\FilesReminders\\Migration\\Version10000Date20230725162149' => $baseDir . '/../lib/Migration/Version10000Date20230725162149.php',
'OCA\\FilesReminders\\Model\\RichReminder' => $baseDir . '/../lib/Model/RichReminder.php',
'OCA\\FilesReminders\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
'OCA\\FilesReminders\\Service\\ReminderService' => $baseDir . '/../lib/Service/ReminderService.php',
);

View file

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = $vendorDir;
return array(
);

View file

@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = $vendorDir;
return array(
'OCA\\FilesReminders\\' => array($baseDir . '/../lib'),
);

View file

@ -0,0 +1,37 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitFilesReminders
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInitFilesReminders', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitFilesReminders', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitFilesReminders::getInitializer($loader));
$loader->setClassMapAuthoritative(true);
$loader->register(true);
return $loader;
}
}

View file

@ -0,0 +1,52 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitFilesReminders
{
public static $prefixLengthsPsr4 = array (
'O' =>
array (
'OCA\\FilesReminders\\' => 19,
),
);
public static $prefixDirsPsr4 = array (
'OCA\\FilesReminders\\' =>
array (
0 => __DIR__ . '/..' . '/../lib',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OCA\\FilesReminders\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\FilesReminders\\BackgroundJob\\CleanUpReminders' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanUpReminders.php',
'OCA\\FilesReminders\\BackgroundJob\\ScheduledNotifications' => __DIR__ . '/..' . '/../lib/BackgroundJob/ScheduledNotifications.php',
'OCA\\FilesReminders\\Command\\ListCommand' => __DIR__ . '/..' . '/../lib/Command/ListCommand.php',
'OCA\\FilesReminders\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
'OCA\\FilesReminders\\Db\\Reminder' => __DIR__ . '/..' . '/../lib/Db/Reminder.php',
'OCA\\FilesReminders\\Db\\ReminderMapper' => __DIR__ . '/..' . '/../lib/Db/ReminderMapper.php',
'OCA\\FilesReminders\\Exception\\NodeNotFoundException' => __DIR__ . '/..' . '/../lib/Exception/NodeNotFoundException.php',
'OCA\\FilesReminders\\Exception\\UserNotFoundException' => __DIR__ . '/..' . '/../lib/Exception/UserNotFoundException.php',
'OCA\\FilesReminders\\Listener\\LoadAdditionalScriptsListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalScriptsListener.php',
'OCA\\FilesReminders\\Listener\\NodeDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/NodeDeletedListener.php',
'OCA\\FilesReminders\\Listener\\UserDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/UserDeletedListener.php',
'OCA\\FilesReminders\\Migration\\Version10000Date20230725162149' => __DIR__ . '/..' . '/../lib/Migration/Version10000Date20230725162149.php',
'OCA\\FilesReminders\\Model\\RichReminder' => __DIR__ . '/..' . '/../lib/Model/RichReminder.php',
'OCA\\FilesReminders\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
'OCA\\FilesReminders\\Service\\ReminderService' => __DIR__ . '/..' . '/../lib/Service/ReminderService.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitFilesReminders::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitFilesReminders::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitFilesReminders::$classMap;
}, null, ClassLoader::class);
}
}

View file

@ -0,0 +1,5 @@
{
"packages": [],
"dev": false,
"dev-package-names": []
}

View file

@ -0,0 +1,23 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '9685b49f0d8f9e7d34f299e51628748a04d0e175',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
'dev' => false,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '9685b49f0d8f9e7d34f299e51628748a04d0e175',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="M12.5 8H11v6l4.75 2.85.75-1.23-4-2.37zm4.837-6.19l4.607 3.845-1.28 1.535-4.61-3.843zm-10.674 0l1.282 1.536L3.337 7.19l-1.28-1.536zM12 4c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zm0 16c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7z" />
</svg>

After

Width:  |  Height:  |  Size: 415 B

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\AppInfo;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\FilesReminders\Listener\LoadAdditionalScriptsListener;
use OCA\FilesReminders\Listener\NodeDeletedListener;
use OCA\FilesReminders\Listener\UserDeletedListener;
use OCA\FilesReminders\Notification\Notifier;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Files\Events\Node\NodeDeletedEvent;
use OCP\User\Events\UserDeletedEvent;
class Application extends App implements IBootstrap {
public const APP_ID = 'files_reminders';
public function __construct() {
parent::__construct(static::APP_ID);
}
public function boot(IBootContext $context): void {
}
public function register(IRegistrationContext $context): void {
$context->registerNotifierService(Notifier::class);
$context->registerEventListener(NodeDeletedEvent::class, NodeDeletedListener::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalScriptsListener::class);
}
}

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\BackgroundJob;
use OCA\FilesReminders\Service\ReminderService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
class CleanUpReminders extends TimedJob {
public function __construct(
ITimeFactory $time,
private ReminderService $reminderService,
) {
parent::__construct($time);
$this->setInterval(24 * 60 * 60); // 1 day
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function run($argument) {
$this->reminderService->cleanUp(500);
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\BackgroundJob;
use OCA\FilesReminders\Db\ReminderMapper;
use OCA\FilesReminders\Service\ReminderService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\Job;
use Psr\Log\LoggerInterface;
class ScheduledNotifications extends Job {
public function __construct(
ITimeFactory $time,
protected ReminderMapper $reminderMapper,
protected ReminderService $reminderService,
protected LoggerInterface $logger,
) {
parent::__construct($time);
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function run($argument) {
$reminders = $this->reminderMapper->findOverdue();
foreach ($reminders as $reminder) {
try {
$this->reminderService->send($reminder);
} catch (DoesNotExistException $e) {
$this->logger->debug('Could not send notification for reminder with id ' . $reminder->getId());
}
}
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Command;
use DateTimeInterface;
use OC\Core\Command\Base;
use OCA\FilesReminders\Model\RichReminder;
use OCA\FilesReminders\Service\ReminderService;
use OCP\IUserManager;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class ListCommand extends Base {
public function __construct(
private ReminderService $reminderService,
private IUserManager $userManager,
) {
parent::__construct();
}
protected function configure(): void {
$this
->setName('files:reminders')
->setDescription('List file reminders')
->addArgument(
'user',
InputArgument::OPTIONAL,
'list reminders for user',
)
->addOption(
'output',
null,
InputOption::VALUE_OPTIONAL,
'Output format (plain, json or json_pretty, default is plain)',
$this->defaultOutputFormat,
);
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$io = new SymfonyStyle($input, $output);
$uid = $input->getArgument('user');
if ($uid !== null) {
/** @var string $uid */
$user = $this->userManager->get($uid);
if ($user === null) {
$io->error("Unknown user <$uid>");
return 1;
}
}
$reminders = $this->reminderService->getAll($user ?? null);
if (empty($reminders)) {
$io->text('No reminders');
return 0;
}
$outputOption = $input->getOption('output');
switch ($outputOption) {
case static::OUTPUT_FORMAT_JSON:
case static::OUTPUT_FORMAT_JSON_PRETTY:
$this->writeArrayInOutputFormat(
$input,
$io,
array_map(
fn (RichReminder $reminder) => $reminder->jsonSerialize(),
$reminders,
),
'',
);
return 0;
default:
$io->table(
['User Id', 'File Id', 'Path', 'Due Date', 'Updated At', 'Created At', 'Notified'],
array_map(
fn (RichReminder $reminder) => [
$reminder->getUserId(),
$reminder->getFileId(),
$reminder->getNode()->getPath(),
$reminder->getDueDate()->format(DateTimeInterface::ATOM), // ISO 8601
$reminder->getUpdatedAt()->format(DateTimeInterface::ATOM), // ISO 8601
$reminder->getCreatedAt()->format(DateTimeInterface::ATOM), // ISO 8601
$reminder->getNotified() ? 'true' : 'false',
],
$reminders,
),
);
return 0;
}
}
}

View file

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Controller;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use Exception;
use OCA\FilesReminders\Exception\NodeNotFoundException;
use OCA\FilesReminders\Service\ReminderService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
class ApiController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
protected ReminderService $reminderService,
protected IUserSession $userSession,
protected LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
/**
* Get a reminder
*/
public function get(int $fileId): DataResponse {
$user = $this->userSession->getUser();
if ($user === null) {
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
}
try {
$reminder = $this->reminderService->getDueForUser($user, $fileId);
$reminderData = [
'dueDate' => $reminder->getDueDate()->format(DateTimeInterface::ATOM), // ISO 8601
];
return new DataResponse($reminderData, Http::STATUS_OK);
} catch (DoesNotExistException $e) {
$reminderData = [
'dueDate' => null,
];
return new DataResponse($reminderData, Http::STATUS_OK);
}
}
/**
* Set a reminder
*
* @param string $dueDate ISO 8601 formatted date time string
*/
public function set(int $fileId, string $dueDate): DataResponse {
try {
$dueDate = (new DateTime($dueDate))->setTimezone(new DateTimeZone('UTC'));
} catch (Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$user = $this->userSession->getUser();
if ($user === null) {
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
}
try {
$created = $this->reminderService->createOrUpdate($user, $fileId, $dueDate);
if ($created) {
return new DataResponse([], Http::STATUS_CREATED);
}
return new DataResponse([], Http::STATUS_OK);
} catch (NodeNotFoundException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
}
/**
* Remove a reminder
*/
public function remove(int $fileId): DataResponse {
$user = $this->userSession->getUser();
if ($user === null) {
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
}
try {
$this->reminderService->remove($user, $fileId);
return new DataResponse([], Http::STATUS_OK);
} catch (DoesNotExistException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
}
}

View file

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Db;
use DateTime;
use OCP\AppFramework\Db\Entity;
/**
* @method void setUserId(string $userId)
* @method string getUserId()
*
* @method void setFileId(int $fileId)
* @method int getFileId()
*
* @method void setDueDate(DateTime $dueDate)
* @method DateTime getDueDate()
*
* @method void setUpdatedAt(DateTime $updatedAt)
* @method DateTime getUpdatedAt()
*
* @method void setCreatedAt(DateTime $createdAt)
* @method DateTime getCreatedAt()
*
* @method void setNotified(bool $notified)
* @method bool getNotified()
*/
class Reminder extends Entity {
protected $userId;
protected $fileId;
protected $dueDate;
protected $updatedAt;
protected $createdAt;
protected $notified = false;
public function __construct() {
$this->addType('userId', 'string');
$this->addType('fileId', 'integer');
$this->addType('dueDate', 'datetime');
$this->addType('updatedAt', 'datetime');
$this->addType('createdAt', 'datetime');
$this->addType('notified', 'boolean');
}
}

View file

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Db;
use DateTime;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Node;
use OCP\IDBConnection;
use OCP\IUser;
/**
* @template-extends QBMapper<Reminder>
*/
class ReminderMapper extends QBMapper {
public const TABLE_NAME = 'files_reminders';
public function __construct(IDBConnection $db) {
parent::__construct(
$db,
static::TABLE_NAME,
Reminder::class,
);
}
public function markNotified(Reminder $reminder): Reminder {
$reminderUpdate = new Reminder();
$reminderUpdate->setId($reminder->getId());
$reminderUpdate->setNotified(true);
return $this->update($reminderUpdate);
}
public function find(int $id): Reminder {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
->from($this->getTableName())
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
return $this->findEntity($qb);
}
/**
* @throws DoesNotExistException
*/
public function findDueForUser(IUser $user, int $fileId): Reminder {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
->from($this->getTableName())
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)))
->andWhere($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)));
return $this->findEntity($qb);
}
/**
* @return Reminder[]
*/
public function findAll() {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
->from($this->getTableName())
->orderBy('due_date', 'ASC');
return $this->findEntities($qb);
}
/**
* @return Reminder[]
*/
public function findAllForUser(IUser $user) {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
->from($this->getTableName())
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)))
->orderBy('due_date', 'ASC');
return $this->findEntities($qb);
}
/**
* @return Reminder[]
*/
public function findAllForNode(Node $node) {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
->from($this->getTableName())
->where($qb->expr()->eq('file_id', $qb->createNamedParameter($node->getId(), IQueryBuilder::PARAM_INT)))
->orderBy('due_date', 'ASC');
return $this->findEntities($qb);
}
/**
* @return Reminder[]
*/
public function findOverdue() {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
->from($this->getTableName())
->where($qb->expr()->lt('due_date', $qb->createFunction('NOW()')))
->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
->orderBy('due_date', 'ASC');
return $this->findEntities($qb);
}
/**
* @return Reminder[]
*/
public function findNotified(DateTime $buffer, ?int $limit = null) {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
->from($this->getTableName())
->where($qb->expr()->eq('notified', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->lt('due_date', $qb->createNamedParameter($buffer, IQueryBuilder::PARAM_DATE)))
->orderBy('due_date', 'ASC')
->setMaxResults($limit);
return $this->findEntities($qb);
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Exception;
use Exception;
class NodeNotFoundException extends Exception {
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Exception;
use Exception;
class UserNotFoundException extends Exception {
}

View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Listener;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\FilesReminders\AppInfo\Application;
use OCP\App\IAppManager;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;
class LoadAdditionalScriptsListener implements IEventListener {
public function __construct(
private IAppManager $appManager,
) {}
public function handle(Event $event): void {
if (!($event instanceof LoadAdditionalScriptsEvent)) {
return;
}
if (!$this->appManager->isEnabledForUser('notifications')) {
return;
}
Util::addScript(Application::APP_ID, 'main');
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Listener;
use OC\Files\Node\NonExistingFile;
use OC\Files\Node\NonExistingFolder;
use OCA\FilesReminders\Service\ReminderService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\Node\NodeDeletedEvent;
class NodeDeletedListener implements IEventListener {
public function __construct(
private ReminderService $reminderService,
) {}
public function handle(Event $event): void {
if (!($event instanceof NodeDeletedEvent)) {
return;
}
$node = $event->getNode();
if ($node instanceof NonExistingFile || $node instanceof NonExistingFolder) {
return;
}
$this->reminderService->removeAllForNode($node);
}
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Listener;
use OCA\FilesReminders\Service\ReminderService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\User\Events\UserDeletedEvent;
class UserDeletedListener implements IEventListener {
public function __construct(
private ReminderService $reminderService,
) {}
public function handle(Event $event): void {
if (!($event instanceof UserDeletedEvent)) {
return;
}
$user = $event->getUser();
$this->reminderService->removeAllForUser($user);
}
}

View file

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Migration;
use Closure;
use OCA\FilesReminders\Db\ReminderMapper;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version10000Date20230725162149 extends SimpleMigrationStep {
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable(ReminderMapper::TABLE_NAME)) {
return null;
}
$table = $schema->createTable(ReminderMapper::TABLE_NAME);
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
'unsigned' => true,
]);
$table->addColumn('user_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('file_id', Types::BIGINT, [
'notnull' => true,
'length' => 20,
'unsigned' => true,
]);
$table->addColumn('due_date', Types::DATETIME, [
'notnull' => true,
]);
$table->addColumn('updated_at', Types::DATETIME, [
'notnull' => true,
]);
$table->addColumn('created_at', Types::DATETIME, [
'notnull' => true,
]);
$table->addColumn('notified', Types::BOOLEAN, [
'notnull' => false,
'default' => false,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['user_id', 'file_id', 'due_date'], 'reminders_uniq_idx');
return $schema;
}
}

View file

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Model;
use DateTimeInterface;
use JsonSerializable;
use OCA\FilesReminders\Db\Reminder;
use OCA\FilesReminders\Exception\NodeNotFoundException;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
class RichReminder extends Reminder implements JsonSerializable {
public function __construct(
private Reminder $reminder,
private IRootFolder $root,
) {
parent::__construct();
}
/**
* @throws NodeNotFoundException
*/
public function getNode(): Node {
$nodes = $this->root->getUserFolder($this->getUserId())->getById($this->getFileId());
if (empty($nodes)) {
throw new NodeNotFoundException();
}
$node = reset($nodes);
return $node;
}
protected function getter(string $name): mixed {
return $this->reminder->getter($name);
}
public function __call(string $methodName, array $args) {
return $this->reminder->__call($methodName, $args);
}
public function jsonSerialize(): array {
return [
'userId' => $this->getUserId(),
'fileId' => $this->getFileId(),
'path' => $this->getNode()->getPath(),
'dueDate' => $this->getDueDate()->format(DateTimeInterface::ATOM), // ISO 8601
'updatedAt' => $this->getUpdatedAt()->format(DateTimeInterface::ATOM), // ISO 8601
'createdAt' => $this->getCreatedAt()->format(DateTimeInterface::ATOM), // ISO 8601
'notified' => $this->getNotified(),
];
}
}

View file

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Notification;
use InvalidArgumentException;
use OCA\FilesReminders\AppInfo\Application;
use OCP\Files\FileInfo;
use OCP\Files\IRootFolder;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\IAction;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
class Notifier implements INotifier {
public function __construct(
protected IFactory $l10nFactory,
protected IURLGenerator $urlGenerator,
protected IRootFolder $root,
) {}
public function getID(): string {
return Application::APP_ID;
}
public function getName(): string {
return $this->l10nFactory->get(Application::APP_ID)->t('File reminders');
}
/**
* @throws InvalidArgumentException
* @throws AlreadyProcessedException
*/
public function prepare(INotification $notification, string $languageCode): INotification {
$l = $this->l10nFactory->get(Application::APP_ID, $languageCode);
if ($notification->getApp() !== Application::APP_ID) {
throw new InvalidArgumentException();
}
switch ($notification->getSubject()) {
case 'reminder-due':
$params = $notification->getSubjectParameters();
$fileId = $params['fileId'];
$nodes = $this->root->getUserFolder($notification->getUser())->getById($fileId);
if (empty($nodes)) {
throw new InvalidArgumentException();
}
$node = reset($nodes);
$path = rtrim($node->getPath(), '/');
if (strpos($path, '/' . $notification->getUser() . '/files/') === 0) {
// Remove /user/files/...
$fullPath = $path;
[,,, $path] = explode('/', $fullPath, 4);
}
$link = $this->urlGenerator->linkToRouteAbsolute(
'files.viewcontroller.showFile',
['fileid' => $node->getId()],
);
// TRANSLATORS The name placeholder is for a file or folder name
$subject = $l->t('Reminder for {name}');
$notification
->setRichSubject(
$subject,
[
'name' => [
'type' => 'highlight',
'id' => $node->getId(),
'name' => $node->getName(),
],
],
)
->setLink($link);
$label = match ($node->getType()) {
FileInfo::TYPE_FILE => $l->t('View file'),
FileInfo::TYPE_FOLDER => $l->t('View folder'),
};
$this->addActionButton($notification, $label);
break;
default:
throw new InvalidArgumentException();
break;
}
return $notification;
}
protected function addActionButton(INotification $notification, string $label): void {
$action = $notification->createAction();
$action->setLabel($label)
->setParsedLabel($label)
->setLink($notification->getLink(), IAction::TYPE_WEB)
->setPrimary(true);
$notification->addParsedAction($action);
}
}

View file

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\FilesReminders\Service;
use DateTime;
use DateTimeZone;
use OCA\FilesReminders\AppInfo\Application;
use OCA\FilesReminders\Db\Reminder;
use OCA\FilesReminders\Db\ReminderMapper;
use OCA\FilesReminders\Exception\NodeNotFoundException;
use OCA\FilesReminders\Exception\UserNotFoundException;
use OCA\FilesReminders\Model\RichReminder;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager as INotificationManager;
use Psr\Log\LoggerInterface;
use Throwable;
class ReminderService {
public function __construct(
protected IUserManager $userManager,
protected IURLGenerator $urlGenerator,
protected INotificationManager $notificationManager,
protected ReminderMapper $reminderMapper,
protected IRootFolder $root,
protected LoggerInterface $logger,
) {}
/**
* @throws DoesNotExistException
*/
public function get(int $id): RichReminder {
$reminder = $this->reminderMapper->find($id);
return new RichReminder($reminder, $this->root);
}
/**
* @throws DoesNotExistException
*/
public function getDueForUser(IUser $user, int $fileId): RichReminder {
$reminder = $this->reminderMapper->findDueForUser($user, $fileId);
return new RichReminder($reminder, $this->root);
}
/**
* @return RichReminder[]
*/
public function getAll(?IUser $user = null) {
$reminders = ($user !== null)
? $this->reminderMapper->findAllForUser($user)
: $this->reminderMapper->findAll();
return array_map(
fn (Reminder $reminder) => new RichReminder($reminder, $this->root),
$reminders,
);
}
/**
* @return bool true if created, false if updated
*
* @throws NodeNotFoundException
*/
public function createOrUpdate(IUser $user, int $fileId, DateTime $dueDate): bool {
$now = new DateTime('now', new DateTimeZone('UTC'));
try {
$reminder = $this->reminderMapper->findDueForUser($user, $fileId);
$reminder->setDueDate($dueDate);
$reminder->setUpdatedAt($now);
$this->reminderMapper->update($reminder);
return false;
} catch (DoesNotExistException $e) {
$nodes = $this->root->getUserFolder($user->getUID())->getById($fileId);
if (empty($nodes)) {
throw new NodeNotFoundException();
}
// Create new reminder if no reminder is found
$reminder = new Reminder();
$reminder->setUserId($user->getUID());
$reminder->setFileId($fileId);
$reminder->setDueDate($dueDate);
$reminder->setUpdatedAt($now);
$reminder->setCreatedAt($now);
$this->reminderMapper->insert($reminder);
return true;
}
}
/**
* @throws DoesNotExistException
*/
public function remove(IUser $user, int $fileId): void {
$reminder = $this->reminderMapper->findDueForUser($user, $fileId);
$this->reminderMapper->delete($reminder);
}
public function removeAllForNode(Node $node): void {
$reminders = $this->reminderMapper->findAllForNode($node);
foreach ($reminders as $reminder) {
$this->reminderMapper->delete($reminder);
}
}
public function removeAllForUser(IUser $user): void {
$reminders = $this->reminderMapper->findAllForUser($user);
foreach ($reminders as $reminder) {
$this->reminderMapper->delete($reminder);
}
}
/**
* @throws DoesNotExistException
* @throws UserNotFoundException
*/
public function send(Reminder $reminder): void {
if ($reminder->getNotified()) {
return;
}
$user = $this->userManager->get($reminder->getUserId());
if ($user === null) {
throw new UserNotFoundException();
}
$notification = $this->notificationManager->createNotification();
$notification
->setApp(Application::APP_ID)
->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('files', 'folder.svg')))
->setUser($user->getUID())
->setObject('reminder', (string)$reminder->getId())
->setSubject('reminder-due', [
'fileId' => $reminder->getFileId(),
])
->setDateTime($reminder->getDueDate());
try {
$this->notificationManager->notify($notification);
$this->reminderMapper->markNotified($reminder);
} catch (Throwable $th) {
$this->logger->error($th->getMessage(), $th->getTrace());
}
}
public function cleanUp(?int $limit = null): void {
$buffer = (new DateTime())
->setTimezone(new DateTimeZone('UTC'))
->modify('-1 day');
$reminders = $this->reminderMapper->findNotified($buffer, $limit);
foreach ($reminders as $reminder) {
$this->reminderMapper->delete($reminder);
}
}
}

View file

@ -0,0 +1,272 @@
<!--
- @copyright 2023 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<NcActions class="actions-secondary-vue"
:open.sync="open">
<NcActionButton @click="$emit('back')">
<template #icon>
<ArrowLeft :size="20" />
</template>
{{ t('files_reminders', 'Back') }}
</NcActionButton>
<NcActionButton v-if="Boolean(dueDate)"
:aria-label="clearAriaLabel"
@click="clear">
<template #icon>
<CloseCircleOutline :size="20" />
</template>
{{ t('files_reminders', 'Clear reminder') }} {{ getDateString(dueDate) }}
</NcActionButton>
<NcActionSeparator />
<NcActionButton v-for="({ label, ariaLabel, dateString, action }) in options"
:key="label"
:aria-label="ariaLabel"
@click="action">
{{ label }} {{ dateString }}
</NcActionButton>
<NcActionSeparator />
<NcActionInput type="datetime-local"
is-native-picker
:min="now"
v-model="customDueDate">
<template #icon>
<CalendarClock :size="20" />
</template>
</NcActionInput>
<NcActionButton :aria-label="customAriaLabel"
@click="setCustom">
<template #icon>
<Check :size="20" />
</template>
{{ t('files_reminders', 'Set custom reminder') }}
</NcActionButton>
</NcActions>
</template>
<script lang="ts">
import Vue, { type PropType } from 'vue'
import { translate as t } from '@nextcloud/l10n'
import { showError, showSuccess } from '@nextcloud/dialogs'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js'
import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
import CalendarClock from 'vue-material-design-icons/CalendarClock.vue'
import Check from 'vue-material-design-icons/Check.vue'
import CloseCircleOutline from 'vue-material-design-icons/CloseCircleOutline.vue'
import { clearReminder, setReminder } from '../services/reminderService.ts'
import {
DateTimePreset,
getDateString,
getDateTime,
getInitialCustomDueDate,
getVerboseDateString,
} from '../shared/utils.ts'
import { logger } from '../shared/logger.ts'
import type { FileAttributes } from '../shared/types.ts'
interface ReminderOption {
dateTimePreset: DateTimePreset
label: string
ariaLabel: string
dateString?: string
action?: () => Promise<void>
}
const laterToday: ReminderOption = {
dateTimePreset: DateTimePreset.LaterToday,
label: t('files_reminders', 'Later today'),
ariaLabel: t('files_reminders', 'Set reminder for later today'),
}
const tomorrow: ReminderOption = {
dateTimePreset: DateTimePreset.Tomorrow,
label: t('files_reminders', 'Tomorrow'),
ariaLabel: t('files_reminders', 'Set reminder for tomorrow'),
}
const thisWeekend: ReminderOption = {
dateTimePreset: DateTimePreset.ThisWeekend,
label: t('files_reminders', 'This weekend'),
ariaLabel: t('files_reminders', 'Set reminder for this weekend'),
}
const nextWeek: ReminderOption = {
dateTimePreset: DateTimePreset.NextWeek,
label: t('files_reminders', 'Next week'),
ariaLabel: t('files_reminders', 'Set reminder for next week'),
}
export default Vue.extend({
name: 'SetReminderActions',
components: {
ArrowLeft,
CalendarClock,
Check,
CloseCircleOutline,
NcActionButton,
NcActionInput,
NcActions,
NcActionSeparator,
},
props: {
file: {
type: Object as PropType<FileAttributes>,
required: true,
},
dueDate: {
type: Date as PropType<null | Date>,
default: null,
},
},
data() {
return {
open: true,
now: new Date(),
customDueDate: getInitialCustomDueDate() as '' | Date,
}
},
watch: {
open(isOpen) {
if (!isOpen) {
this.$emit('close')
}
},
},
computed: {
fileId(): number {
return this.file.id
},
fileName(): string {
return this.file.name
},
clearAriaLabel(): string {
return `${t('files_reminders', 'Clear reminder')} ${getVerboseDateString(this.dueDate as Date)}`
},
customAriaLabel(): null | string {
if (this.customDueDate === '') {
return null
}
return `${t('files_reminders', 'Set reminder at custom date & time')} ${getVerboseDateString(this.customDueDate)}`
},
options(): ReminderOption[] {
const computeOption = (option: ReminderOption): null | ReminderOption => {
const dateTime = getDateTime(option.dateTimePreset)
if (!dateTime) {
return null
}
return {
...option,
ariaLabel: `${option.ariaLabel} ${getVerboseDateString(dateTime)}`,
dateString: getDateString(dateTime),
action: () => this.set(dateTime),
}
}
const options = [
laterToday,
tomorrow,
thisWeekend,
nextWeek,
]
return options
.map(computeOption)
.filter(Boolean) as ReminderOption[]
},
},
methods: {
t,
getDateString,
async set(dueDate: Date): Promise<void> {
try {
await setReminder(this.fileId, dueDate)
showSuccess(t('files_reminders', 'Reminder set for "{fileName}"', { fileName: this.fileName }))
this.open = false
} catch (error) {
logger.error('Failed to set reminder', { error })
showError(t('files_reminders', 'Failed to set reminder'))
}
},
async setCustom(): Promise<void> {
// Handle input cleared
if (this.customDueDate === '') {
showError(t('files_reminders', 'Please choose a valid date & time'))
return
}
try {
await setReminder(this.fileId, this.customDueDate)
showSuccess(t('files_reminders', 'Reminder set for "{fileName}"', { fileName: this.fileName }))
this.open = false
} catch (error) {
logger.error('Failed to set reminder', { error })
showError(t('files_reminders', 'Failed to set reminder'))
}
},
async clear(): Promise<void> {
try {
await clearReminder(this.fileId)
showSuccess(t('files_reminders', 'Reminder cleared'))
this.open = false
} catch (error) {
logger.error('Failed to clear reminder', { error })
showError(t('files_reminders', 'Failed to clear reminder'))
}
},
},
})
</script>
<style lang="scss" scoped>
.actions-secondary-vue {
display: block !important;
float: right !important;
padding: 5px 0 0 4px !important;
pointer-events: none !important; // prevent activation of file row
}
</style>

View file

@ -0,0 +1,102 @@
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue, { type ComponentInstance } from 'vue'
import { subscribe } from '@nextcloud/event-bus'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import SetReminderActionsComponent from './components/SetReminderActions.vue'
import { getReminder } from './services/reminderService.js'
import { logger } from './shared/logger.js'
import type { FileAttributes } from './shared/types.js'
interface FileContext {
[key: string]: any
$file: JQuery<HTMLTableRowElement>
fileInfoModel: {
[key: string]: any
attributes: FileAttributes
}
}
interface EventPayload {
el: HTMLDivElement
context: FileContext
}
const handleOpen = async (payload: EventPayload) => {
const fileId = payload.context.fileInfoModel.attributes.id
const menuEl = payload.context.$file[0].querySelector('.fileactions .action-menu') as HTMLLinkElement
const linkEl = payload.el.querySelector('.action-setreminder-container .action-setreminder') as HTMLLinkElement
let dueDate: null | Date = null
let error: null | any = null
try {
dueDate = (await getReminder(fileId)).dueDate
} catch (e) {
error = e
logger.error(`Failed to load reminder for file with id: ${fileId}`, { error })
}
linkEl.addEventListener('click', (_event) => {
if (error) {
showError(t('files_reminders', 'Failed to load reminder'))
throw Error()
}
const mountPoint = document.createElement('div')
const SetReminderActions = Vue.extend(SetReminderActionsComponent)
const origDisplay = menuEl.style.display
menuEl.style.display = 'none'
menuEl.insertAdjacentElement('afterend', mountPoint)
const propsData = {
file: payload.context.fileInfoModel.attributes,
dueDate,
}
const actions = (new SetReminderActions({ propsData }) as ComponentInstance)
.$mount(mountPoint)
const cleanUp = () => {
actions.$destroy() // destroy popper
actions.$el.remove() // remove action menu button
menuEl.style.display = origDisplay
}
actions.$once('back', () => {
cleanUp()
menuEl.click() // reopen original actions menu
})
actions.$once('close', () => {
cleanUp()
})
}, {
once: true,
})
}
subscribe('files:action-menu:opened', handleOpen)

View file

@ -0,0 +1,55 @@
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
interface Reminder {
dueDate: null | Date
}
export const getReminder = async (fileId: number): Promise<Reminder> => {
const url = generateOcsUrl('/apps/files_reminders/api/v1/get/{fileId}', { fileId })
const response = await axios.get(url)
const dueDate = response.data.ocs.data.dueDate ? new Date(response.data.ocs.data.dueDate) : null
return {
dueDate,
}
}
export const setReminder = async (fileId: number, dueDate: Date): Promise<[]> => {
const url = generateOcsUrl('/apps/files_reminders/api/v1/set/{fileId}', { fileId })
const response = await axios.put(url, {
dueDate: dueDate.toISOString(), // timezone of string is always UTC
})
return response.data.ocs.data
}
export const clearReminder = async (fileId: number): Promise<[]> => {
const url = generateOcsUrl('/apps/files_reminders/api/v1/remove/{fileId}', { fileId })
const response = await axios.delete(url)
return response.data.ocs.data
}

View file

@ -0,0 +1,28 @@
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getLoggerBuilder } from '@nextcloud/logger'
export const logger = getLoggerBuilder()
.setApp('files_reminders')
.detectUser()
.build()

View file

@ -0,0 +1,27 @@
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
export interface FileAttributes {
[key: string]: any
id: number
name: string
}

View file

@ -0,0 +1,142 @@
/**
* @copyright 2023 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import moment from '@nextcloud/moment'
import { getCanonicalLocale } from '@nextcloud/l10n'
export enum DateTimePreset {
LaterToday,
Tomorrow,
ThisWeekend,
NextWeek,
}
export const getDateTime = (dateTime: DateTimePreset): null | Date => {
const matchPreset: Record<DateTimePreset, () => null | Date> = {
[DateTimePreset.LaterToday]: () => {
const now = moment()
const evening = moment()
.startOf('day')
.add(18, 'hour')
const cutoff = evening
.clone()
.subtract(1, 'hour')
if (now.isSameOrAfter(cutoff)) {
return null
}
return evening.toDate()
},
[DateTimePreset.Tomorrow]: () => {
const day = moment()
.add(1, 'day')
.startOf('day')
.add(8, 'hour')
return day.toDate()
},
[DateTimePreset.ThisWeekend]: () => {
const today = moment()
if (
[
5, // Friday
6, // Saturday
7, // Sunday
].includes(today.isoWeekday())
) {
return null
}
const saturday = moment()
.startOf('isoWeek')
.add(5, 'day')
.add(8, 'hour')
return saturday.toDate()
},
[DateTimePreset.NextWeek]: () => {
const today = moment()
if (today.isoWeekday() === 7) { // Sunday
return null
}
const workday = moment()
.startOf('isoWeek')
.add(1, 'week')
.add(8, 'hour')
return workday.toDate()
},
}
return matchPreset[dateTime]()
}
export const getInitialCustomDueDate = (): Date => {
const hour = moment().get('hour')
const dueDate = moment()
.startOf('day')
.add(hour + 2, 'hour')
return dueDate.toDate()
}
export const getDateString = (dueDate: Date): string => {
let formatOptions: Intl.DateTimeFormatOptions = {
hour: 'numeric',
minute: '2-digit',
}
const dueDateMoment = moment(dueDate)
const today = moment()
if (!dueDateMoment.isSame(today, 'date')) {
formatOptions = {
...formatOptions,
weekday: 'short',
}
}
if (!dueDateMoment.isSame(today, 'week')) {
formatOptions = {
...formatOptions,
month: 'short',
day: 'numeric',
}
}
return dueDate.toLocaleString(
getCanonicalLocale(),
formatOptions,
)
}
export const getVerboseDateString = (dueDate: Date): string => {
const formatOptions: Intl.DateTimeFormatOptions = {
weekday: 'long',
hour: 'numeric',
minute: '2-digit',
month: 'long',
day: 'numeric',
}
return dueDate.toLocaleString(
getCanonicalLocale(),
formatOptions,
)
}

View file

@ -178,6 +178,7 @@ OC.L10N.register(
"Expires {relativetime}" : "{زمان نسبی } منقضی می شود",
"this share just expired." : "این اشتراک تازه منقضی شد",
"Shared with you by {owner}" : "به اشتراک گذاشته شده با شما توسط { دارنده}",
"Open in Files" : "در فایل باز کنید",
"Link to a file" : "پیوند به یک پرونده",
"Error creating the share" : "خطایی در ایجاد اشتراک",
"Error updating the share" : "خطایی در به روزرسانی اشتراک",

View file

@ -176,6 +176,7 @@
"Expires {relativetime}" : "{زمان نسبی } منقضی می شود",
"this share just expired." : "این اشتراک تازه منقضی شد",
"Shared with you by {owner}" : "به اشتراک گذاشته شده با شما توسط { دارنده}",
"Open in Files" : "در فایل باز کنید",
"Link to a file" : "پیوند به یک پرونده",
"Error creating the share" : "خطایی در ایجاد اشتراک",
"Error updating the share" : "خطایی در به روزرسانی اشتراک",

View file

@ -65,7 +65,7 @@ OC.L10N.register(
"{actor} shared {file} with {user}" : "{actor} a partagé {file} avec {user}",
"{actor} removed {user} from {file}" : "{actor} a supprimé {user} de {file}",
"{actor} shared {file} with you" : "{actor} a partagé {file} avec vous",
"{actor} removed you from the share named {file}" : "{actor} vous a supprimé du partage du nom de {file}",
"{actor} removed you from the share named {file}" : "{actor} vous a retiré du partage nommé {file}",
"Share for file {file} with {user} expired" : "Partage du fichier {file} avec l'utilisateur {user} expiré",
"Share for file {file} expired" : "Partage du fichier {file} expiré",
"A file or folder shared by mail or by public link was <strong>downloaded</strong>" : "Un fichier ou un dossier partagé par e-mail ou par lien public a été <strong>téléchargé</strong>",

View file

@ -63,7 +63,7 @@
"{actor} shared {file} with {user}" : "{actor} a partagé {file} avec {user}",
"{actor} removed {user} from {file}" : "{actor} a supprimé {user} de {file}",
"{actor} shared {file} with you" : "{actor} a partagé {file} avec vous",
"{actor} removed you from the share named {file}" : "{actor} vous a supprimé du partage du nom de {file}",
"{actor} removed you from the share named {file}" : "{actor} vous a retiré du partage nommé {file}",
"Share for file {file} with {user} expired" : "Partage du fichier {file} avec l'utilisateur {user} expiré",
"Share for file {file} expired" : "Partage du fichier {file} expiré",
"A file or folder shared by mail or by public link was <strong>downloaded</strong>" : "Un fichier ou un dossier partagé par e-mail ou par lien public a été <strong>téléchargé</strong>",

Some files were not shown because too many files have changed in this diff Show more