OpenAI vision API from FWH

Post Reply
User avatar
Antonio Linares
Site Admin
Posts: 42252
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

OpenAI vision API from FWH

Post by Antonio Linares »

Working fine! :-)

Using https://platform.openai.com/docs/guides/vision examples.

openai1.prg

Code: Select all | Expand

#include "FiveWin.ch"
#include "c:\harbour\contrib\hbcurl\hbcurl.ch"

//----------------------------------------------------------------------------//

function Main()

    local oChatgpt := TOpenAI():New()

    oChatgpt:SendImageURL( "https://forums.fivetechsupport.com/styles/prosilver/imageset/site_logo.gif" )

    MsgInfo( oChatgpt:GetValue() )

    oChatgpt:End()

return nil    

//----------------------------------------------------------------------------//

CLASS TOpenAI
    
    DATA   cKey   INIT ""
    DATA   cModel INIT "gpt-4o-mini"
    DATA   cResponse
    DATA   cUrl
    DATA   hCurl
    DATA   nError INIT 0
    DATA   nHttpCode INIT 0

    METHOD New( cKey, cModel )
    METHOD SendImageURL( cImageUrl )
    METHOD End()    
    METHOD GetValue( cHKey )    

ENDCLASS        

//----------------------------------------------------------------------------//

METHOD New( cKey, cModel ) CLASS TOpenAI

    if Empty( cKey )
       ::cKey = GetEnv( "OPENAI_API_KEY" )
    endif

    if ! Empty( cModel )
       ::cModel = cModel
    endif
    
    ::cUrl = "https://api.openai.com/v1/chat/completions"
    ::hCurl = curl_easy_init()
    
return Self    

//----------------------------------------------------------------------------//

METHOD End() CLASS TOpenAI

    curl_easy_cleanup( ::hCurl )
    ::hCurl = nil

return nil    

//----------------------------------------------------------------------------//

METHOD GetValue( cHKey ) CLASS TOpenAI

    local aKeys := hb_AParams(), cKey
    local uValue := hb_jsonDecode( ::cResponse )

    hb_default( @cHKey, "content" )

    if cHKey == "content"
       uValue = uValue[ "choices" ][ 1 ][ "message" ][ "content" ]
    endif

    TRY
       for each cKey in aKeys
          if ValType( uValue[ cKey ] ) == "A"
             uValue = uValue[ cKey ][ 1 ][ "choices" ][ 1 ][ "message" ][ "content" ]
          else
             uValue = uValue[ cKey ]
          endif
       next
    CATCH
       XBrowser( uValue )
    END

return uValue

//----------------------------------------------------------------------------//

METHOD SendImageURL( cImageUrl ) CLASS TOpenAI

    local aHeaders, cJson, hRequest := { => }, hMessage := { => }

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl )

    aHeaders := { "Content-Type: application/json", ;
                  "Authorization: Bearer " + ::cKey }

    curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, aHeaders )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_USERNAME, "" )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )

    hb_HSet( hRequest, "model", ::cModel )
    hb_HSet( hMessage, "role", "user" )

    hb_HSet( hMessage, "content", { ;
        { "type" => "text", "text" => "What's in this image?" }, ;
        { "type" => "image_url", "image_url" => { "url" => cImageUrl } } } )

    hRequest[ "messages" ] = { hMessage }

    cJson = hb_jsonEncode( hRequest )

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )

    ::nError = curl_easy_perform( ::hCurl )
    curl_easy_getinfo( ::hCurl, HB_CURLINFO_RESPONSE_CODE, @::nHttpCode )

    if ::nError == HB_CURLE_OK
       ::cResponse = curl_easy_dl_buff_get( ::hCurl )
    else
       ::cResponse := "Error code " + Str( ::nError )
    endif

return ::cResponse

//----------------------------------------------------------------------------//
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 42252
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI vision API from FWH

Post by Antonio Linares »

Class TOpenAI Method SendImage( cImageFileName ) // Please notice BMPs are not allowed

openai1.prg

Code: Select all | Expand

#include "FiveWin.ch"
#include "c:\harbour\contrib\hbcurl\hbcurl.ch"

//----------------------------------------------------------------------------//

function Main()

    local oChatgpt := TOpenAI():New()

    // oChatgpt:SendImageURL( "https://forums.fivetechsupport.com/styles/prosilver/imageset/site_logo.gif" )
    oChatgpt:SendImage( "../bitmaps/logo/fivepowergm5.jpg" )

    MsgInfo( oChatgpt:GetValue() )

    oChatgpt:End()

return nil    

//----------------------------------------------------------------------------//

CLASS TOpenAI
    
    DATA   cKey   INIT ""
    DATA   cModel INIT "gpt-4o-mini"
    DATA   cResponse
    DATA   cUrl
    DATA   hCurl
    DATA   nError INIT 0
    DATA   nHttpCode INIT 0

    METHOD New( cKey, cModel )
    METHOD SendImage( cImageFileName )    
    METHOD SendImageURL( cImageUrl )
    METHOD End()    
    METHOD GetValue( cHKey )    

ENDCLASS        

//----------------------------------------------------------------------------//

METHOD New( cKey, cModel ) CLASS TOpenAI

    if Empty( cKey )
       ::cKey = GetEnv( "OPENAI_API_KEY" )
    endif

    if ! Empty( cModel )
       ::cModel = cModel
    endif
    
    ::cUrl = "https://api.openai.com/v1/chat/completions"
    ::hCurl = curl_easy_init()
    
return Self    

//----------------------------------------------------------------------------//

METHOD End() CLASS TOpenAI

    curl_easy_cleanup( ::hCurl )
    ::hCurl = nil

return nil    

//----------------------------------------------------------------------------//

METHOD GetValue( cHKey ) CLASS TOpenAI

    local aKeys := hb_AParams(), cKey
    local uValue := hb_jsonDecode( ::cResponse )

    hb_default( @cHKey, "content" )

    if cHKey == "content"
       uValue = uValue[ "choices" ][ 1 ][ "message" ][ "content" ]
    endif

    TRY
       for each cKey in aKeys
          if ValType( uValue[ cKey ] ) == "A"
             uValue = uValue[ cKey ][ 1 ][ "choices" ][ 1 ][ "message" ][ "content" ]
          else
             uValue = uValue[ cKey ]
          endif
       next
    CATCH
       XBrowser( uValue )
    END

return uValue

//----------------------------------------------------------------------------//

METHOD SendImage( cImageFileName ) CLASS TOpenAI

    local aHeaders, cJson, cBase64Image, hMessage := { => }

    if ! File( cImageFileName )
       MsgAlert( "Image " + cImageFileName + " not found" )
       return nil
    endif

    cBase64Image := hb_base64Encode( memoRead( cImageFileName ) )

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, "https://api.openai.com/v1/chat/completions" )

    aHeaders := { "Content-Type: application/json", ;
                  "Authorization: Bearer " + ::cKey }

    curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, aHeaders )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_USERNAME, "" )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )

    hb_HSet( hMessage, "role", "user" )
    hb_HSet( hMessage, "content", { ;
        { "type" => "text", "text" => "What is in this image?" }, ;
        { "type" => "image_url", ;
          "image_url" => { "url" => "data:image/jpeg;base64," + cBase64Image } } } )

    cJson := hb_jsonEncode( { "model" => ::cModel, ;
                              "messages" => { hMessage } } )

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )

    ::nError = curl_easy_perform( ::hCurl )
    curl_easy_getinfo( ::hCurl, HB_CURLINFO_RESPONSE_CODE, @::nHttpCode )

    if ::nError == HB_CURLE_OK
       ::cResponse = curl_easy_dl_buff_get( ::hCurl )
    else
       ::cResponse := "Error code " + Str( ::nError )
    endif

return ::cResponse

//----------------------------------------------------------------------------//

METHOD SendImageURL( cImageUrl ) CLASS TOpenAI

    local aHeaders, cJson, hRequest := { => }, hMessage := { => }

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl )

    aHeaders := { "Content-Type: application/json", ;
                  "Authorization: Bearer " + ::cKey }

    curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, aHeaders )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_USERNAME, "" )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )

    hb_HSet( hRequest, "model", ::cModel )
    hb_HSet( hMessage, "role", "user" )

    hb_HSet( hMessage, "content", { ;
        { "type" => "text", "text" => "What's in this image?" }, ;
        { "type" => "image_url", "image_url" => { "url" => cImageUrl } } } )

    hRequest[ "messages" ] = { hMessage }

    cJson = hb_jsonEncode( hRequest )

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )

    ::nError = curl_easy_perform( ::hCurl )
    curl_easy_getinfo( ::hCurl, HB_CURLINFO_RESPONSE_CODE, @::nHttpCode )

    if ::nError == HB_CURLE_OK
       ::cResponse = curl_easy_dl_buff_get( ::hCurl )
    else
       ::cResponse := "Error code " + Str( ::nError )
    endif

return ::cResponse

//----------------------------------------------------------------------------//
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 42252
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI vision API from FWH

Post by Antonio Linares »

METHOD Send( cPrompt ) CLASS TOpenAI implemented

Before running this example please set your OpenAI user key from your CMD window:
set OPENAI_API_KEY=sk-...

openai1.prg

Code: Select all | Expand

#include "FiveWin.ch"
#include "c:\harbour\contrib\hbcurl\hbcurl.ch"

//----------------------------------------------------------------------------//

function Main()

    local oChatgpt := TOpenAI():New()

    // oChatgpt:SendImageURL( "https://forums.fivetechsupport.com/styles/prosilver/imageset/site_logo.gif" )
    // oChatgpt:SendImage( "../bitmaps/logo/fivepowergm5.jpg" )
    oChatgpt:Send( "What is the capital of France ?" )

    MsgInfo( oChatgpt:GetValue() )

    oChatgpt:End()

return nil    

//----------------------------------------------------------------------------//

CLASS TOpenAI
    
    DATA   cKey   INIT ""
    DATA   cModel INIT "gpt-4o-mini"
    DATA   cResponse
    DATA   cUrl
    DATA   hCurl
    DATA   nError INIT 0
    DATA   nHttpCode INIT 0

    METHOD New( cKey, cModel )
    METHOD Send( cPrompt )    
    METHOD SendImage( cImageFileName )    
    METHOD SendImageURL( cImageUrl )
    METHOD End()    
    METHOD GetValue( cHKey )    

ENDCLASS        

//----------------------------------------------------------------------------//

METHOD New( cKey, cModel ) CLASS TOpenAI

    if Empty( cKey )
       ::cKey = GetEnv( "OPENAI_API_KEY" )
    endif

    if ! Empty( cModel )
       ::cModel = cModel
    endif
    
    ::cUrl = "https://api.openai.com/v1/chat/completions"
    ::hCurl = curl_easy_init()
    
return Self    

//----------------------------------------------------------------------------//

METHOD End() CLASS TOpenAI

    curl_easy_cleanup( ::hCurl )
    ::hCurl = nil

return nil    

//----------------------------------------------------------------------------//

METHOD GetValue( cHKey ) CLASS TOpenAI

    local aKeys := hb_AParams(), cKey
    local uValue := hb_jsonDecode( ::cResponse )

    hb_default( @cHKey, "content" )

    if cHKey == "content"
       uValue = uValue[ "choices" ][ 1 ][ "message" ][ "content" ]
    endif

    TRY
       for each cKey in aKeys
          if ValType( uValue[ cKey ] ) == "A"
             uValue = uValue[ cKey ][ 1 ][ "choices" ][ 1 ][ "message" ][ "content" ]
          else
             uValue = uValue[ cKey ]
          endif
       next
    CATCH
       XBrowser( uValue )
    END

return uValue

//----------------------------------------------------------------------------//

METHOD Send( cPrompt ) CLASS TOpenAI 

    LOCAL aHeaders, cJson, hRequest := { => }, hMessage := { => }

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl )

    aHeaders := { "Content-Type: application/json", ;
                  "Authorization: Bearer " + ::cKey }

    curl_easy_setopt(::hCurl, HB_CURLOPT_HTTPHEADER, aHeaders)
    curl_easy_setopt(::hCurl, HB_CURLOPT_USERNAME, '')
    curl_easy_setopt(::hCurl, HB_CURLOPT_DL_BUFF_SETUP)
    curl_easy_setopt(::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F.)

    hb_HSet( hRequest, "model", ::cModel )
    hb_HSet( hMessage, "role", "user" )
    hb_HSet( hMessage, "content", cPrompt )

    hRequest[ "messages" ] = { hMessage }
    cJson = hb_jsonEncode( hRequest )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )
    ::nError = curl_easy_perform( ::hCurl )
    curl_easy_getinfo( ::hCurl, HB_CURLINFO_RESPONSE_CODE, @::nHttpCode )

    if ::nError == HB_CURLE_OK
       ::cResponse = curl_easy_dl_buff_get( ::hCurl )
    else
       ::cResponse := "Error code " + Str( ::nError )
    endif
 
return ::cResponse

//----------------------------------------------------------------------------//

METHOD SendImage( cImageFileName ) CLASS TOpenAI

    local aHeaders, cJson, cBase64Image, hMessage := { => }

    if ! File( cImageFileName )
       MsgAlert( "Image " + cImageFileName + " not found" )
       return nil
    endif

    cBase64Image := hb_base64Encode( memoRead( cImageFileName ) )

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, "https://api.openai.com/v1/chat/completions" )

    aHeaders := { "Content-Type: application/json", ;
                  "Authorization: Bearer " + ::cKey }

    curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, aHeaders )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_USERNAME, "" )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )

    hb_HSet( hMessage, "role", "user" )
    hb_HSet( hMessage, "content", { ;
        { "type" => "text", "text" => "What is in this image?" }, ;
        { "type" => "image_url", ;
          "image_url" => { "url" => "data:image/jpeg;base64," + cBase64Image } } } )

    cJson := hb_jsonEncode( { "model" => ::cModel, ;
                              "messages" => { hMessage } } )

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )

    ::nError = curl_easy_perform( ::hCurl )
    curl_easy_getinfo( ::hCurl, HB_CURLINFO_RESPONSE_CODE, @::nHttpCode )

    if ::nError == HB_CURLE_OK
       ::cResponse = curl_easy_dl_buff_get( ::hCurl )
    else
       ::cResponse = "Error code " + Str( ::nError )
    endif

return ::cResponse

//----------------------------------------------------------------------------//

METHOD SendImageURL( cImageUrl ) CLASS TOpenAI

    local aHeaders, cJson, hRequest := { => }, hMessage := { => }

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POST, .T. )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_URL, ::cUrl )

    aHeaders := { "Content-Type: application/json", ;
                  "Authorization: Bearer " + ::cKey }

    curl_easy_setopt( ::hCurl, HB_CURLOPT_HTTPHEADER, aHeaders )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_USERNAME, "" )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_DL_BUFF_SETUP )
    curl_easy_setopt( ::hCurl, HB_CURLOPT_SSL_VERIFYPEER, .F. )

    hb_HSet( hRequest, "model", ::cModel )
    hb_HSet( hMessage, "role", "user" )

    hb_HSet( hMessage, "content", { ;
        { "type" => "text", "text" => "What's in this image?" }, ;
        { "type" => "image_url", "image_url" => { "url" => cImageUrl } } } )

    hRequest[ "messages" ] = { hMessage }

    cJson = hb_jsonEncode( hRequest )

    curl_easy_setopt( ::hCurl, HB_CURLOPT_POSTFIELDS, cJson )

    ::nError = curl_easy_perform( ::hCurl )
    curl_easy_getinfo( ::hCurl, HB_CURLINFO_RESPONSE_CODE, @::nHttpCode )

    if ::nError == HB_CURLE_OK
       ::cResponse = curl_easy_dl_buff_get( ::hCurl )
    else
       ::cResponse = "Error code " + Str( ::nError )
    endif

return ::cResponse

//----------------------------------------------------------------------------//
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 42252
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI vision API from FWH

Post by Antonio Linares »

Next: our first AI Agent demo ;-)
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Carles
Posts: 1146
Joined: Fri Feb 10, 2006 2:34 pm
Location: Barcelona
Contact:

Re: OpenAI vision API from FWH

Post by Carles »

Hi,

Great !

C.
Salutacions, saludos, regards

"...programar es fácil, hacer programas es difícil..."

UT Page -> https://carles9000.github.io/
Forum UT -> https://discord.gg/bq8a9yGMWh
Skype -> https://join.skype.com/cnzQg3Kr1dnk
User avatar
Otto
Posts: 6378
Joined: Fri Oct 07, 2005 7:07 pm
Contact:

Re: OpenAI vision API from FWH

Post by Otto »

Dear Antonio,

Thank you very much for your research work.

A few days ago, I saw an advertisement for the Jetson Nano Orin. Do you think it would be possible to use this hardware to run a custom LMM and then send requests using FIVEWIN?

For example:

Emails are read via a local tool (e.g., Outlook or a custom email parser) and provided in a structured form (e.g., text files or via an API).

**Summary:**
The language model analyzes the content of the emails and generates a short summary (e.g., "New reservation for room 12 from January 10th to 12th.").

**Presentation:**
The summarized information could be displayed in a user-friendly interface, e.g., in a FIVEWIN application.

**Additional Features:**
- **Keyword Detection:** Highlighting important terms such as names, reservation dates, or invoice inquiries.
- **Categorization:** Sorting emails by topics (e.g., bookings, complaints, inquiries).

Best regards,
Otto
********************************************************************
mod harbour - Vamos a la conquista de la Web
modharbour.org
https://www.facebook.com/groups/modharbour.club
********************************************************************
User avatar
Antonio Linares
Site Admin
Posts: 42252
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI vision API from FWH

Post by Antonio Linares »

Dear Otto,

Nvidia's Jetson Nano Orin uses Linux (a special ubuntu version) so I have just asked copilot if ollama may be installed on it:
Yes, it is possible to use Ollama on the Jetson Nano Orin. Ollama is an open-source tool that allows you to run large language models (LLMs) locally on Jetson devices, including the Jetson Nano Orin.

To install Ollama on your Jetson Nano Orin, you can follow these steps:

1. **Preparation**:
- Ensure you have JetPack 6.0 installed on your device.
- Download the SD card image from the official NVIDIA website.

2. **Native Installation**:
- Use the official Ollama installer:
```bash
curl -fsSL https://ollama.com/install.sh | sh
```
- This will create a service to run `ollama serve` at startup, allowing you to use the `ollama` command immediately.

3. **Using Docker Containers**:
- You can run Ollama inside a Docker container to avoid changes to your existing system:
```bash
docker run --runtime nvidia --rm --network=host -v ~/ollama:/ollama -e OLLAMA_MODELS=/ollama dustynv/ollama:r36.2.0
```

By following these steps, you can fully utilize the capabilities of your Jetson Nano Orin to run large language models efficiently.
We can use ollama from Harbour :)
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 42252
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI vision API from FWH

Post by Antonio Linares »

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Eroni
Posts: 100
Joined: Fri Jul 21, 2006 7:15 pm
Location: Criciuma/SC Brazil
Contact:

Re: OpenAI vision API from FWH

Post by Eroni »

Hello all.

I am trying build exe from openai1.prg but experiencing I am linking errors: (unresolved external)

Code: Select all | Expand

openai1.obj : error LNK2001: símbolo externo não resolvido _HB_FUN_CURL_EASY_INIT
openai1.obj : error LNK2001: símbolo externo não resolvido _HB_FUN_CURL_EASY_CLEANUP
openai1.obj : error LNK2001: símbolo externo não resolvido _HB_FUN_CURL_EASY_SETOPT
openai1.obj : error LNK2001: símbolo externo não resolvido _HB_FUN_CURL_EASY_PERFORM
openai1.obj : error LNK2001: símbolo externo não resolvido _HB_FUN_CURL_EASY_GETINFO
openai1.obj : error LNK2001: símbolo externo não resolvido _HB_FUN_CURL_EASY_DL_BUFF_GET
Harbour 32 and MSVC 2022.

Which libs is missing?

Thanks in advance.
FWH 1709 BCC72 MySql MariaDB
Visual Studio 2019 / Xamarin / C#
User avatar
Antonio Linares
Site Admin
Posts: 42252
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: OpenAI vision API from FWH

Post by Antonio Linares »

hbcurl.lib and libcurl.lib
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Eroni
Posts: 100
Joined: Fri Jul 21, 2006 7:15 pm
Location: Criciuma/SC Brazil
Contact:

Re: OpenAI vision API from FWH

Post by Eroni »

Antonio Linares wrote: Tue Jan 07, 2025 2:16 pm hbcurl.lib and libcurl.lib
Thanks!
FWH 1709 BCC72 MySql MariaDB
Visual Studio 2019 / Xamarin / C#
Post Reply