FastComments.com

Add Comments to Your iOS App

Ово је званична iOS библиотека за FastComments.

Уграђујте видгете за уживо коментарисање, ћаскање и рецензије у вашу iOS апликацију.

Репозиторијум

Погледајте на GitHub


Функције Internal Link


  • Стабло коментара са угнежденим одговорима и пагинацијом
  • Друштвени фид са креирањем објава, реакцијама и прилозима медија
  • Режим уживо ћаскања са аутоматским скроловањем и раздвајачима датума
  • Ажурирања у реалном времену преко WebSocket-а (нови коментари, гласови, присутност)
  • Јединствено пријављивање (Simple SSO за тестирање, Secure SSO за продукцију)
  • Уређивање богатог текста са подебљаним, курзивом, кодом и @mentions
  • Гласање са конфигурисаним стиловима (стрелице горе/доле или срца)
  • Модерацијске акције: пријави, прикачи, закључај, блокирај
  • Свеобухватно подешавање тема са предефинисаним опцијама и потпуном прилагодбом
  • Прилагођена дугмад на алатној траци за коментаре и креирање објава у фиду
  • Отпремање слика
  • Подршка за EU регион
  • Присутност корисника (индикатори online/offline)
  • Филтрирање фида по ознакама
  • Подршка за локализацију

Захтеви Internal Link


  • iOS 16+ или macOS 14+
  • Swift 5.9+
  • SwiftUI

Инсталација Internal Link

Додајте FastCommentsUI у ваш пројекат користећи Swift Package Manager.

У Xcode: File > Add Package Dependencies, затим унесите URL репозиторијума.

Или додајте у ваш Package.swift:

dependencies: [
    .package(url: "https://github.com/fastcomments/fastcomments-ios.git", from: "1.0.0")
]

Затим додајте product у ваш target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "FastCommentsUI", package: "fastcomments-ios")
    ]
)

Увезите оба модула где је потребно:

import FastCommentsUI
import FastCommentsSwift

Брзи почетак Internal Link

Минимално подешавање за приказ коментарског видгета:

import SwiftUI
import FastCommentsUI

struct ContentView: View {
    @StateObject private var sdk = FastCommentsSDK(
        config: FastCommentsWidgetConfig(
            tenantId: "demo",
            urlId: "my-page-1",
            url: "https://example.com/page-1",
            pageTitle: "My Page"
        )
    )

    var body: some View {
        FastCommentsView(sdk: sdk)
            .task {
                try? await sdk.load()
            }
    }
}

Замените "demo" својим FastComments tenant ID-ом. urlId идентификује страницу или нит (thread) у којој се чувају коментари.

Аутентификација (SSO) Internal Link

FastComments подржава три режима аутентификације:

  1. Anonymous -- нема SSO токена; корисници добијају идентитете засноване на сесији
  2. Simple SSO -- клијентски токен за демонстрације и тестирање (није безбедан)
  3. Secure SSO -- серверски потписан токен за продукцију

Simple SSO

Корисно за демонстрације и локално тестирање. Са Simple SSO свако може да се представи као било који корисник, па га не користите у продукцији.

import FastCommentsSwift

let userData = SimpleSSOUserData(
    username: "Jane Doe",
    email: "jane@example.com",
    avatar: "https://example.com/avatar.jpg"
)
let sso = FastCommentsSSO.createSimple(simpleSSOUserData: userData)
let token = try? sso.prepareToSend()

let config = FastCommentsWidgetConfig(
    tenantId: "YOUR_TENANT_ID",
    urlId: "my-page-1",
    sso: token
)
let sdk = FastCommentsSDK(config: config)

SimpleSSOUserData такође подржава опционална поља:

  • id -- ID корисника (подразумева се на адресу е-поште ако није подешено)
  • displayName -- посебно име за приказ
  • displayLabel -- прилагођена ознака приказана поред имена (нпр. "VIP")
  • websiteUrl -- веза на корисниково име
  • locale -- код локала
  • isProfileActivityPrivate -- сакриј активност профила (подразумевано true)

Secure SSO

У продукцији, ваш бекенд генерише потписан SSO токен користећи ваш API секрет. iOS апликација преузима овај токен са вашег сервера и прослеђује га у конфигурацију.

On your backend (using the FastComments Swift SDK or any language):

let userData = SecureSSOUserData(
    id: "user-123",
    email: "user@example.com",
    username: "Display Name",
    avatar: "https://example.com/avatar.jpg"
)
let sso = try FastCommentsSSO.createSecure(apiKey: "YOUR_API_KEY", secureSSOUserData: userData)
let token = try sso.prepareToSend()
// Вратите овај токен вашој iOS апликацији преко вашег API-ја

In your iOS app:

struct MyView: View {
    @StateObject private var sdk = FastCommentsSDK(
        config: FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "my-page-1"
        )
    )
    @State private var isLoadingToken = true

    var body: some View {
        Group {
            if isLoadingToken {
                ProgressView("Loading...")
            } else {
                FastCommentsView(sdk: sdk)
            }
        }
        .task {
            // Преузмите токен са вашег бекенда
            let token = try? await fetchSSOTokenFromYourBackend()
            // Креирајте нову конфигурацију са токеном, или је подесите пре учитавања
            isLoadingToken = false
            try? await sdk.load()
        }
    }
}

SecureSSOUserData подржава додатна поља:

  • optedInNotifications -- пријава за обавештења путем е-поште
  • displayLabel -- прилагођена ознака
  • displayName -- име за приказ
  • websiteUrl -- URL веб-сајта
  • groupIds -- чланства у групама
  • isAdmin -- администраторска права
  • isModerator -- права модератора
  • isProfileActivityPrivate -- приватност активности профила

Коментари у нитима Internal Link

Основна употреба

struct CommentsPage: View {
    @StateObject private var sdk = FastCommentsSDK(
        config: FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "article-42",
            url: "https://example.com/article/42",
            pageTitle: "Article Title"
        )
    )

    var body: some View {
        FastCommentsView(sdk: sdk)
            .task {
                try? await sdk.load()
            }
    }
}

Стилови гласања

Подразумевани стил гласања приказује стрелице горе/доле. Проследите ._1 за стил гласања са срцем:

FastCommentsView(sdk: sdk, voteStyle: ._1)
Стил Изглед
._0 Дугмад са стрелицама горе/доле са нето резултатом
._1 Једно дугме у облику срца са бројем

Повратни позиви догађаја

Користите повратне позиве у облику модификатора за обраду интеракција са корисником:

FastCommentsView(sdk: sdk)
    .onCommentPosted { comment in
        print("New comment: \(comment.commentHTML)")
    }
    .onReplyClick { renderableComment in
        print("Replying to: \(renderableComment.comment.id)")
    }
    .onUserClick { context, userInfo, source in
        // source је .name или .avatar
        print("Tapped \(userInfo.displayName)")
    }

Примена теме

Проследите тему преко SwiftUI окружења:

FastCommentsView(sdk: sdk)
    .fastCommentsTheme(myTheme)
    .task { try? await sdk.load() }

Или је подесите директно у SDK:

sdk.theme = FastCommentsTheme.modern

Смер сортирања

sdk.defaultSortDirection = .nf  // Најновије прво (подразумевано)
sdk.defaultSortDirection = .of  // Најстарије прво
sdk.defaultSortDirection = .mr  // Најрелевантније


Разговор уживо Internal Link

LiveChatView пружа разговор у реалном времену са аутоматским скроловањем, раздвајачима датума и компактним изгледом. Аутоматски конфигурише SDK за сортирање са најстаријим прво и тренутни приказ уживо.

struct ChatView: View {
    @StateObject private var sdk: FastCommentsSDK = {
        let config = FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "chat-room-1",
            sso: ssoToken  // SSO се препоручује како би корисници имали имена
        )
        return FastCommentsSDK(config: config)
    }()

    var body: some View {
        LiveChatView(sdk: sdk)
            .onCommentPosted { comment in
                print("Sent: \(comment.commentHTML)")
            }
            .task {
                try? await sdk.load()
            }
    }
}

LiveChatView подржава следеће повратне позиве:

  • .onCommentPosted -- покреће се када корисник пошаље поруку
  • .onCommentDeleted -- покреће се када се порука избрише
  • .onUserClick -- покреће се када је корисничко име или аватар додирнут(и)

Друштвени фид Internal Link

Систем фида је посебан SDK (FastCommentsFeedSDK) са својим приказом.

Учитавање и приказивање фида

struct FeedPage: View {
    @StateObject private var sdk: FastCommentsFeedSDK = {
        let config = FastCommentsWidgetConfig(
            tenantId: "YOUR_TENANT_ID",
            urlId: "my-feed",
            sso: ssoToken
        )
        return FastCommentsFeedSDK(config: config)
    }()

    @State private var commentsPost: FeedPost?

    var body: some View {
        FastCommentsFeedView(sdk: sdk)
            .onPostSelected { post in
                commentsPost = post
            }
            .onCommentsRequested { post in
                commentsPost = post
            }
            .onSharePost { post in
                // Прикажи дијалог за дељење
            }
            .onUserClick { context, userInfo, source in
                // Навигирај до корисничког профила
            }
            .onMediaClick { mediaItem, index in
                // Прикажи прегледач слика преко целог екрана
            }
            .task {
                try? await sdk.load()
            }
    }
}

Преглед фида аутоматски садржи повлачење за освежавање и бесконачно скроловање.

Креирање објава

Користите FeedPostCreateView да прикажете форму за креирање објаве:

@State private var showCreatePost = false

// In your view body:
.sheet(isPresented: $showCreatePost) {
    FeedPostCreateView(
        sdk: sdk,
        onPostCreated: { post in
            showCreatePost = false
            Task { try? await sdk.refresh() }
        },
        onCancelled: {
            showCreatePost = false
        }
    )
}

Реакције на објаве

SDK рукује реакцијама са оптимистичким ажурирањима:

try await sdk.reactPost(postId: post.id, reactionType: "l")

// Check reaction state
let hasLiked = sdk.hasUserReacted(postId: post.id, reactType: "l")
let likeCount = sdk.getLikeCount(postId: post.id)

Отварање коментара за објаву

Користите CommentsSheet за приказ коментара за објаву из фида. Он интерно креира инстанцу FastCommentsSDK користећи конфигурацију фид SDK-а:

.sheet(item: $commentsPost) { post in
    CommentsSheet(post: post, feedSDK: sdk, onUserClick: { context, userInfo, source in
        // Обради кликање на корисника
    })
}

Напомена: FeedPost мора да подржава Identifiable за .sheet(item:). Додајте ово проширење:

extension FeedPost: @retroactive Identifiable {}

Филтрирање фида по таговима

Имплементирајте протокол TagSupplier да бисте филтрирали објаве у фиду по таговима:

struct TeamTagSupplier: TagSupplier {
    func getTags(currentUser: UserSessionInfo?) -> [String]? {
        guard let user = currentUser else { return nil }
        return ["team:\(user.id ?? "")", "public"]
    }
}

sdk.tagSupplier = TeamTagSupplier()

Вратите nil да бисте добили нефилтрирани глобални фид.

Чување и враћање стања фида

Чувајте стање пагинације кроз догађаје животног циклуса приказа:

let state = sdk.savePaginationState()
// Later...
sdk.restorePaginationState(state)

Брисање објава

sdk.onPostDeleted = { postId in
    print("Post \(postId) was deleted")
}

Прилагођавање тема Internal Link

Унапред дефинисане теме

Доступне су четири уграђене преднаподешене теме:

// Системске подразумеване вредности
sdk.theme = FastCommentsTheme.default

// Картице са сенкама и великим заобљеним угловима
sdk.theme = FastCommentsTheme.modern

// Плоснато, без сенки, мали заобљени угао, без линија нити
sdk.theme = FastCommentsTheme.minimal

// Постави све боје акција на једну боју бренда
sdk.theme = FastCommentsTheme.allPrimary(.indigo)

Стилови приказа коментара

var theme = FastCommentsTheme()
theme.commentStyle = .flat    // Плосната листа са раздвајачима (подразумевано)
theme.commentStyle = .card    // Заобљене картице са сенкама
theme.commentStyle = .bubble  // Стил балона за ћаскање

Боје

Сва својства боја су опциони. Ако нису подешена, користе се разумне системске подразумеване вредности.

var theme = FastCommentsTheme()

// Боје бренда
theme.primaryColor = .indigo
theme.primaryLightColor = .indigo.opacity(0.6)
theme.primaryDarkColor = Color(red: 0.2, green: 0.1, blue: 0.5)

// Позадине
theme.commentBackgroundColor = Color(.secondarySystemGroupedBackground)
theme.containerBackgroundColor = Color(.systemGroupedBackground)

// Дугмад за акције
theme.actionButtonColor = .indigo
theme.replyButtonColor = .indigo
theme.toggleRepliesButtonColor = .indigo.opacity(0.8)
theme.loadMoreButtonTextColor = .indigo

// Гласови
theme.voteActiveColor = .red
theme.voteCountColor = .primary
theme.voteCountZeroColor = .secondary
theme.voteDividerColor = Color(.separator)

// Везе
theme.linkColor = .indigo
theme.linkColorPressed = .indigo.opacity(0.5)

// Дијалози
theme.dialogHeaderBackgroundColor = .indigo
theme.dialogHeaderTextColor = .white

// Трака за унос
theme.inputBarBackgroundColor = Color(.systemBackground)
theme.inputBarBorderColor = Color(.separator)

// Остало
theme.onlineIndicatorColor = .green
theme.separatorColor = Color(.separator)
theme.badgeBackgroundColor = .gray.opacity(0.2)
theme.threadLineColor = .indigo.opacity(0.15)

Типографија

theme.commenterNameFont = .subheadline.weight(.bold)
theme.bodyFont = .body
theme.captionFont = .caption
theme.actionFont = .caption.weight(.medium)

Распоред и размак

theme.cornerRadius = .large       // .none, .small, .medium, .large
theme.commentSpacing = 4          // Тачке између редова коментара
theme.nestingIndent = 20          // Тачке увлачења по нивоу угнежђавања
theme.avatarSize = 36             // Дијаметар аватара за коренске коментаре
theme.replyAvatarSize = 28        // Дијаметар аватара за угнежђене одговоре

Визуелни ефекти

theme.showShadows = true          // Нежне сенке на картицама
theme.showThreadLine = true       // Вертикална линија која повезује угнежђене одговоре
theme.animateVotes = true         // Еластична анимација при променама гласа

Примена тема

Два приступа:

// Via SwiftUI environment (recommended for view hierarchy)
FastCommentsView(sdk: sdk)
    .fastCommentsTheme(theme)

// Directly on the SDK
sdk.theme = theme

Прилагођена дугмадa на траци са алаткама Internal Link

Дугмад траке за коментаре

Имплементирајте протокол CustomToolbarButton да бисте додали дугмад у траку за унос коментара:

struct EmojiButton: CustomToolbarButton {
    let id = "emoji"
    let iconSystemName = "face.smiling"       // Назив SF симбола
    let contentDescription = "Add Emoji"
    let badgeText: String? = nil              // Опционални број значке

    func onClick(text: Binding<String>) {
        text.wrappedValue += "\u{1F44D}"
    }

    // Опционална преписивања (подразумевано true)
    func isEnabled() -> Bool { true }
    func isVisible() -> Bool { true }
}

Проследите прилагођена дугмад приликом креирања приказа:

FastCommentsView(
    sdk: sdk,
    customToolbarButtons: [EmojiButton(), CodeBlockButton()]
)

Или их додате глобално у SDK (важи за све инстанце):

sdk.addGlobalCustomToolbarButton(EmojiButton())
sdk.removeGlobalCustomToolbarButton(id: "emoji")
sdk.clearGlobalCustomToolbarButtons()

Дугмад траке фида

Имплементирајте FeedCustomToolbarButton за формулар креирања објаве:

struct HashtagButton: FeedCustomToolbarButton {
    let id = "hashtag"
    let iconSystemName = "number"
    let contentDescription = "Add Hashtag"

    func onClick(content: Binding<String>) {
        content.wrappedValue += "#"
    }
}

Проследите их приказу за креирање:

FeedPostCreateView(
    sdk: sdk,
    customToolbarButtons: [HashtagButton()],
    onPostCreated: { _ in },
    onCancelled: { }
)

Или их поставите глобално на feed SDK:

sdk.globalFeedToolbarButtons = [HashtagButton()]

Модерација Internal Link

Radnje dostupne svim korisnicima

  • Označi/Ukloni oznaku -- prijavi komentar na pregled
try await sdk.flagComment(commentId: commentId)
try await sdk.unflagComment(commentId: commentId)
  • Blokiraj/Odblokiraj -- sakrij sve komentare od korisnika (po posmatraču)
try await sdk.blockUser(commentId: commentId)
try await sdk.unblockUser(commentId: commentId)

Radnje samo za administratore

  • Postavi na vrh/Ukloni sa vrha -- postavi komentar na vrh niti
try await sdk.pinComment(commentId: commentId)
try await sdk.unpinComment(commentId: commentId)
  • Zaključaj/Otključaj -- onemogući nove odgovore na komentar
try await sdk.lockComment(commentId: commentId)
try await sdk.unlockComment(commentId: commentId)

Sve radnje moderacije su takođe dostupne kroz kontekstni meni komentara u korisničkom interfejsu. Administratorske radnje se pojavljuju samo kada je trenutni korisnik administrator sajta (postavljeno putem SSO isAdmin zastavice ili konfiguracije kontrolne table).



Ажурирања у реалном времену Internal Link

Након позива sdk.load(), SDK аутоматски се претплаћује на WebSocket догађаје за конфигурисани urlId. Обрађују се следећи догађаји:

  • Нови коментари, измене и брисања
  • Гласови (нови и повучени)
  • Промене стања приквачења (pin), закључавања, означавања (flag) и блокирања
  • Присутност корисника (придруживање/напуштање)
  • Отварање/затварање нити
  • Доделе значки
  • Ажурирања конфигурације сервера

Контрола приказа уживо

Подразумевано, нови коментари других корисника се појављују одмах:

sdk.showLiveRightAway = true   // Подразумевано: приказати одмах

Поставите ово на false да бисте баферовали нове коментаре иза дугмета "N нових коментара", омогућавајући кориснику да изабере када да их прикаже:

sdk.showLiveRightAway = false

Присутност корисника

Индикатори онлајн/офлајн се аутоматски појављују на аватарима корисника када сервер омогући праћење присутности. На клијенту није потребна додатна конфигурација.



Пагинација Internal Link

Veličina stranice

// Komentari: podrazumevano 30
sdk.pageSize = 50

// Feed: podrazumevano 10
feedSDK.pageSize = 20

Učitavanje više komentara

UI automatski prikazuje kontrole za paginaciju. Takođe možete pokrenuti paginaciju programatski:

// Učitaj sledeću stranicu
try await sdk.loadMore()

// Učitaj sve preostalo (onemogućeno ako ima >2000 komentara zbog performansi)
try await sdk.loadAll()

// Proveri stanje
sdk.hasMore            // Da li postoje još stranica
sdk.shouldShowLoadAll()
sdk.getCountRemainingToShow()

Paginacija podkomentara

Ugnježdeni odgovori se učitavaju po potrebi. Kada korisnik proširi nit, prvih 5 odgovora se učitava. Kontrola "učitaj više odgovora" pojavljuje se ako ima još. Ovo se automatski rešava u korisničkom interfejsu.



Стање и посматљивост Internal Link

Oba FastCommentsSDK и FastCommentsFeedSDK су класе ObservableObject са @Published својствима. Можете их посматрати у својим SwiftUI приказима за реактивна ажурирања корисничког интерфејса.

Objavljena svojstva FastCommentsSDK

Property Type Description
commentCountOnServer Int Ukupan broj komentara na serveru
newRootCommentCount Int Privremeno sačuvani novi komentari (kada је showLiveRightAway false)
currentUser UserSessionInfo? Trenutno autentifikovani korisnik
isSiteAdmin Bool Da li је trenutni korisnik administrator sajta
isClosed Bool Da li је nit komentara zatvorena
hasBillingIssue Bool Da li постоји problem са наплатом
isLoading Bool Да ли је мрежни захтев у току
hasMore Bool Да ли постоје још странице са коментарима
blockingErrorMessage String? Грешка која спречава функционисање корисничког интерфејса
warningMessage String? Неблокирајућа порука упозорења
isDemo Bool Да ли се покреће у демо режиму
commentsVisible Bool Прекидач за видљивост коментара
toolbarEnabled Bool Да ли је алатна трака за форматирање приказана

Objavljena svojstva FastCommentsFeedSDK

Property Type Description
feedPosts [FeedPost] Тренутно учитане објаве фида
hasMore Bool Да ли постоје још странице
currentUser UserSessionInfo? Тренутно аутентификовани корисник
blockingErrorMessage String? Блокирајућа порука о грешци
isLoading Bool Да ли је мрежни захтев у току
newPostsCount Int Број нових објава од последњег учитавања

Stablo komentara

Stablo komentara je dostupno preko sdk.commentsTree:

// Ravna lista vidljivih čvorova za renderovanje
sdk.commentsTree.visibleNodes

// Pronađi komentar po ID-u
sdk.commentsTree.commentsById["comment-id"]

ЕУ регион Internal Link


Да бисте користили ЕУ центар података, подесите поље region у вашој конфигурацији:

let config = FastCommentsWidgetConfig(
    tenantId: "YOUR_TENANT_ID",
    urlId: "my-page",
    region: "eu"
)

Ово усмерава све API захтеве и WebSocket везе на eu.fastcomments.com.



Чишћење Internal Link

Када завршите са инстанцом SDK-а (нпр. када се поглед затвара), позовите cleanup() да бисте затворили WebSocket везу и отказали позадинске задатке:

sdk.cleanup()

За погледе које управља SwiftUI-јев @StateObject, ово се обично позива у .onDisappear или када се поглед уништи.

Отпремање слика Internal Link

Коментари

let imageUrl = try await sdk.uploadImage(imageData: jpegData, filename: "photo.jpg")

Враћа URL (стринг) отпремљене слике.

Објаве у фиду

let mediaItem = try await feedSDK.uploadImage(imageData: jpegData, filename: "photo.jpg")

// Отпремите више слика паралелно
let mediaItems = try await feedSDK.uploadImages(images: [
    (jpegData1, "photo1.jpg"),
    (jpegData2, "photo2.jpg")
])


Помене корисника Internal Link

Претрага корисника за подршку аутоматском довршавању @помена:

let results = try await sdk.searchUsers(query: "jan")
// Враћа [UserSearchResult] са userId, username, avatar итд.

Уграђени CommentInputBar аутоматски обрађује довршавање @помена.

Уређивање и брисање коментара Internal Link


Измени

try await sdk.editComment(commentId: commentId, newText: "Updated text")

Сервер поново рендерује HTML. Локални коментар се аутоматски ажурира.

Обриши

try await sdk.deleteComment(commentId: commentId)

Брисање коментара такође уклања његове потомке из локалног стабла.

Обе радње су доступне преко контекстног менија коментара у корисничком интерфејсу када је тренутни корисник аутор коментара (или администратор сајта).



Руковање грешкама Internal Link

Методе SDK-а бацају FastCommentsError, који имплементира LocalizedError:

do {
    try await sdk.load()
} catch let error as FastCommentsError {
    print(error.translatedError ?? error.reason ?? "Unknown error")
} catch {
    print(error.localizedDescription)
}

FastCommentsError својства:

  • code -- код грешке из API
  • reason -- енглески опис грешке
  • translatedError -- локализована порука о грешци коју обезбеђује сервер

Блокирајуће грешке се такође аутоматски приказују преко sdk.blockingErrorMessage, које уграђени прикази показују кориснику.



Локализација Internal Link


Prosledite kod lokaliteta (locale) u konfiguraciji da biste lokalizovali stringove koje obezbeđuje server:

let config = FastCommentsWidgetConfig(
    tenantId: "YOUR_TENANT_ID",
    urlId: "my-page",
    locale: "fr_fr"
)

Tekstovi korisničkog interfejsa na klijentskoj strani koriste lokalizaciju zasnovanu na iOS bundle-u.


Пример апликације Internal Link


Репозиторијум садржи потпуни пример апликације у ExampleApp/ са демонстрацијама следећег:

  • Коментари у нитима са SSO и прилагођеним темама
  • Друштвени фид са креирањем објава и филтрирањем по ознакама
  • Разговор уживо
  • Једноставни и безбедни SSO токови
  • Прилагођена дугмад на траци са алаткама (коментари и фид)

Trebate pomoć?

Ako naiđete na bilo kakve probleme ili imate pitanja u vezi iOS biblioteke, molimo:

Doprinos

Doprinosi su dobrodošli! Posetite GitHub repozitorijum za smernice o doprinosu.