monorepo + frontend app
This commit is contained in:
Executable
+39
@@ -0,0 +1,39 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/teletype_softwares"
|
||||
cmd = "go build -o ./tmp/teletype_softwares ./main.go"
|
||||
delay = 1000
|
||||
exclude_dir = [
|
||||
"tmp",
|
||||
]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go", "data"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
send_interrupt = false
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
@@ -0,0 +1,21 @@
|
||||
# stage 1: building application binary file
|
||||
FROM --platform=linux/amd64 golang:1.19-alpine as build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go mod tidy
|
||||
RUN go build -o main ./main.go
|
||||
|
||||
# stage 2: copy only the application binary file and necessary files to the alpine container
|
||||
FROM --platform=linux/amd64 alpine:latest
|
||||
RUN apk --update add ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /app/main .
|
||||
COPY --from=build /app/http/openapi.yml .
|
||||
|
||||
# run the service on container startup.
|
||||
CMD ["/app/main"]
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
FROM --platform=linux/amd64 golang:1.23-alpine
|
||||
|
||||
RUN apk add curl git unzip
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN chmod +x ./entrypoint.sh
|
||||
|
||||
RUN curl -fLo install.sh https://raw.githubusercontent.com/cosmtrek/air/master/install.sh \
|
||||
&& chmod +x install.sh && sh install.sh && cp ./bin/air /bin/air
|
||||
|
||||
CMD "./entrypoint.sh"
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"teletype_softwares/lib/mysql_utils"
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
SoftwareRepository SoftwareRepositoryInterface
|
||||
ReleaseRepository ReleaseRepositoryInterface
|
||||
FileRepository FileRepositoryInterface
|
||||
DownloadService DownloadServiceInterface
|
||||
SoftwareUpdaterService SoftwareUpdaterServiceInterface
|
||||
SoftwareService SoftwareServiceInterface
|
||||
FileService FileServiceInterface
|
||||
}
|
||||
|
||||
func NewDomain() Domain {
|
||||
DB := mysql_utils.Init()
|
||||
MigrateGoDatabase(DB)
|
||||
|
||||
software_repository := &SoftwareRepository{db: DB}
|
||||
release_repository := &ReleaseRepository{db: DB}
|
||||
file_repository := NewFileRepository()
|
||||
|
||||
software_service := NewSoftwareService(software_repository, release_repository)
|
||||
|
||||
tic80_updater := NewSoftwareUpdaterTIC80Service(
|
||||
software_repository,
|
||||
release_repository,
|
||||
file_repository,
|
||||
)
|
||||
|
||||
ebitengine_updater := NewSoftwareUpdaterEbitengineService(
|
||||
software_repository,
|
||||
release_repository,
|
||||
file_repository,
|
||||
)
|
||||
|
||||
love_updater := NewSoftwareUpdaterLoveService(
|
||||
software_repository,
|
||||
release_repository,
|
||||
file_repository,
|
||||
)
|
||||
|
||||
software_updater_service := NewSoftwareUpdaterService(tic80_updater, ebitengine_updater, love_updater)
|
||||
|
||||
download_service := NewDownloadService(software_repository, release_repository)
|
||||
|
||||
file_service := NewFileService(file_repository)
|
||||
|
||||
return Domain{
|
||||
SoftwareRepository: software_repository,
|
||||
ReleaseRepository: release_repository,
|
||||
FileRepository: file_repository,
|
||||
DownloadService: download_service,
|
||||
SoftwareUpdaterService: software_updater_service,
|
||||
SoftwareService: software_service,
|
||||
FileService: file_service,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func MigrateGoDatabase(db *gorm.DB) {
|
||||
db.AutoMigrate(
|
||||
Software{},
|
||||
Release{},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Release struct {
|
||||
gorm.Model
|
||||
SoftwareID uint `gorm:"index" json:"softwareId"`
|
||||
Version string `gorm:"size:64" json:"version"`
|
||||
CartridgePath string `gorm:"size:255" json:"cartridgePath"`
|
||||
SourcePath string `gorm:"size:255" json:"sourcePath"`
|
||||
HTMLFolderPath string `gorm:"size:255" json:"htmlFolderPath"`
|
||||
DocsFolderPath string `gorm:"size:255" json:"docsFolderPath"`
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Software struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"uniqueIndex;size:128" json:"name"`
|
||||
Title string `gorm:"size:255" json:"title"`
|
||||
Author string `gorm:"size:255" json:"author"`
|
||||
Desc string `gorm:"type:text" json:"desc"`
|
||||
Site string `gorm:"size:255" json:"site"`
|
||||
License string `gorm:"size:128" json:"license"`
|
||||
Platform string `gorm:"size:128" json:"platform"`
|
||||
Releases []Release `json:"-"`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type FileRepositoryInterface interface {
|
||||
GetPath(path string) string
|
||||
FileExists(path string) bool
|
||||
CreateDir(path string) error
|
||||
DeleteDir(path string) error
|
||||
DeleteFile(path string) error
|
||||
MoveFile(src_path, dest_path string) error
|
||||
UnzipFile(path, dest_path string) error
|
||||
}
|
||||
|
||||
type FileRepository struct {
|
||||
fileContainerPath string
|
||||
}
|
||||
|
||||
func NewFileRepository() *FileRepository {
|
||||
file_container_path, _ := os.LookupEnv("FILE_CONTAINER_PATH")
|
||||
return &FileRepository{
|
||||
fileContainerPath: file_container_path,
|
||||
}
|
||||
}
|
||||
|
||||
func (fr *FileRepository) GetPath(path string) string {
|
||||
return filepath.Join(fr.fileContainerPath, path)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) FileExists(path string) bool {
|
||||
full_path := fr.GetPath(path)
|
||||
_, err := os.Stat(full_path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (fr *FileRepository) CreateDir(path string) error {
|
||||
full_path := fr.GetPath(path)
|
||||
return os.MkdirAll(full_path, 0755)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) DeleteDir(path string) error {
|
||||
full_path := fr.GetPath(path)
|
||||
return os.RemoveAll(full_path)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) DeleteFile(path string) error {
|
||||
full_path := fr.GetPath(path)
|
||||
return os.RemoveAll(full_path)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) MoveFile(src_path, dest_path string) error {
|
||||
full_src_path := fr.GetPath(src_path)
|
||||
full_dest_path := fr.GetPath(dest_path)
|
||||
|
||||
dest_dir := filepath.Dir(full_dest_path)
|
||||
if err := os.MkdirAll(dest_dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create destination directory: %w", err)
|
||||
}
|
||||
|
||||
return os.Rename(full_src_path, full_dest_path)
|
||||
}
|
||||
|
||||
func (fr *FileRepository) UnzipFile(zipfile, desc string) error {
|
||||
zip_path := fr.GetPath(zipfile)
|
||||
desc_path := fr.GetPath(desc)
|
||||
|
||||
if err := os.MkdirAll(desc_path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("unzip", "-q", zip_path, "-d", desc_path)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Remove(zip_path)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type ReleaseRepositoryInterface interface {
|
||||
Create(release *Release) error
|
||||
CreateIfNotExist(release *Release) error
|
||||
FindLatestBySoftwareID(software_id uint) (*Release, error)
|
||||
FindBySoftwareIDAndVersion(software_id uint, version string) (*Release, error)
|
||||
ListBySoftwareID(software_id uint) []Release
|
||||
}
|
||||
|
||||
type ReleaseRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) Create(release *Release) error {
|
||||
return r.db.Create(release).Error
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) CreateIfNotExist(release *Release) error {
|
||||
var existing Release
|
||||
err := r.db.Where("software_id = ? AND version = ?", release.SoftwareID, release.Version).First(&existing).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return r.db.Create(release).Error
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) FindLatestBySoftwareID(software_id uint) (*Release, error) {
|
||||
var release Release
|
||||
err := r.db.Where("software_id = ?", software_id).Order("created_at desc").First(&release).Error
|
||||
return &release, err
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) FindBySoftwareIDAndVersion(software_id uint, version string) (*Release, error) {
|
||||
var release Release
|
||||
err := r.db.Where("software_id = ? AND version = ?", software_id, version).First(&release).Error
|
||||
return &release, err
|
||||
}
|
||||
|
||||
func (r *ReleaseRepository) ListBySoftwareID(software_id uint) []Release {
|
||||
var releases []Release
|
||||
r.db.Where("software_id = ?", software_id).Find(&releases)
|
||||
return releases
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package domain
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type SoftwareRepositoryInterface interface {
|
||||
List() ([]Software, error)
|
||||
GetByName(name string) (*Software, error)
|
||||
UpdateOrCreate(software *Software) error
|
||||
}
|
||||
|
||||
type SoftwareRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *SoftwareRepository) GetByName(name string) (*Software, error) {
|
||||
var software Software
|
||||
if err := r.db.Preload("Releases").Where("name = ?", name).First(&software).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &software, nil
|
||||
}
|
||||
|
||||
func (r *SoftwareRepository) List() ([]Software, error) {
|
||||
var softwares []Software
|
||||
if err := r.db.Preload("Releases").Find(&softwares).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return softwares, nil
|
||||
}
|
||||
|
||||
func (r *SoftwareRepository) UpdateOrCreate(software *Software) error {
|
||||
var existing Software
|
||||
if err := r.db.Where("name = ?", software.Name).First(&existing).Error; err == nil {
|
||||
software.ID = existing.ID
|
||||
return r.db.Model(&existing).Updates(software).Error
|
||||
} else {
|
||||
return r.db.Create(software).Error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSoftwareNotFound = errors.New("software not found")
|
||||
ErrNoReleasesFound = errors.New("no releases found for software")
|
||||
ErrReleaseNotFound = errors.New("release not found for software and version")
|
||||
)
|
||||
|
||||
type DownloadServiceInterface interface {
|
||||
GetLatestRelease(software_name string) (*Release, error)
|
||||
GetSpecificRelease(software_name string, version string) (*Release, error)
|
||||
}
|
||||
|
||||
type DownloadService struct {
|
||||
softwareRepository SoftwareRepositoryInterface
|
||||
releaseRepository ReleaseRepositoryInterface
|
||||
}
|
||||
|
||||
func NewDownloadService(software_repository SoftwareRepositoryInterface, release_repository ReleaseRepositoryInterface) *DownloadService {
|
||||
return &DownloadService{
|
||||
softwareRepository: software_repository,
|
||||
releaseRepository: release_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DownloadService) GetLatestRelease(software_name string) (*Release, error) {
|
||||
software, err := s.softwareRepository.GetByName(software_name)
|
||||
if err != nil {
|
||||
return nil, ErrSoftwareNotFound
|
||||
}
|
||||
|
||||
release, err := s.releaseRepository.FindLatestBySoftwareID(software.ID)
|
||||
if err != nil {
|
||||
return nil, ErrNoReleasesFound
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func (s *DownloadService) GetSpecificRelease(software_name string, version string) (*Release, error) {
|
||||
software, err := s.softwareRepository.GetByName(software_name)
|
||||
if err != nil {
|
||||
return nil, ErrSoftwareNotFound
|
||||
}
|
||||
|
||||
release, err := s.releaseRepository.FindBySoftwareIDAndVersion(software.ID, version)
|
||||
if err != nil {
|
||||
return nil, ErrReleaseNotFound
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package domain
|
||||
|
||||
type FileServiceInterface interface {
|
||||
GetPath(path string) string
|
||||
}
|
||||
|
||||
type FileService struct {
|
||||
FileRepository FileRepositoryInterface
|
||||
}
|
||||
|
||||
func NewFileService(file_repository FileRepositoryInterface) *FileService {
|
||||
return &FileService{
|
||||
FileRepository: file_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileService) GetPath(path string) string {
|
||||
return s.FileRepository.GetPath(path)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type SoftwareListDTO struct {
|
||||
Software
|
||||
LatestRelease *Release
|
||||
}
|
||||
|
||||
type SoftwareDetaildListDTO struct {
|
||||
Softwares []SoftwareShowData `json:"softwares"`
|
||||
}
|
||||
|
||||
type SoftwareShowData struct {
|
||||
Software *Software `json:"software"`
|
||||
Releases []Release `json:"releases"`
|
||||
LatestRelease *Release `json:"latestRelease"`
|
||||
WebPlayableRelease *Release `json:"webPlayableRelease"`
|
||||
}
|
||||
|
||||
type SoftwareServiceInterface interface {
|
||||
List() ([]SoftwareListDTO, error)
|
||||
DetailedList() (*SoftwareDetaildListDTO, error)
|
||||
GetByName(name string) (*Software, error)
|
||||
GetLatestRelease(software_id string) (*Release, error)
|
||||
GetForShowByName(name string) (*SoftwareShowData, error)
|
||||
}
|
||||
|
||||
type SoftwareService struct {
|
||||
softeare_repository SoftwareRepositoryInterface
|
||||
release_repository ReleaseRepositoryInterface
|
||||
}
|
||||
|
||||
func NewSoftwareService(
|
||||
softeare_repository SoftwareRepositoryInterface,
|
||||
release_repository ReleaseRepositoryInterface,
|
||||
) *SoftwareService {
|
||||
return &SoftwareService{
|
||||
softeare_repository: softeare_repository,
|
||||
release_repository: release_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareService) List() ([]SoftwareListDTO, error) {
|
||||
softwares, err := s.softeare_repository.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dtos := make([]SoftwareListDTO, 0, len(softwares))
|
||||
for _, sw := range softwares {
|
||||
dto := SoftwareListDTO{Software: sw}
|
||||
if len(sw.Releases) > 0 {
|
||||
sort.Slice(sw.Releases, func(i, j int) bool {
|
||||
return sw.Releases[i].CreatedAt.After(sw.Releases[j].CreatedAt)
|
||||
})
|
||||
dto.LatestRelease = &sw.Releases[0]
|
||||
}
|
||||
dtos = append(dtos, dto)
|
||||
}
|
||||
|
||||
return dtos, nil
|
||||
}
|
||||
|
||||
func (s *SoftwareService) GetByName(name string) (*Software, error) {
|
||||
return s.softeare_repository.GetByName(name)
|
||||
}
|
||||
|
||||
func (s *SoftwareService) GetForShowByName(name string) (*SoftwareShowData, error) {
|
||||
software, err := s.softeare_repository.GetByName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if software == nil {
|
||||
return nil, fmt.Errorf("software not found")
|
||||
}
|
||||
|
||||
releases := s.release_repository.ListBySoftwareID(software.ID)
|
||||
|
||||
var latest_release *Release
|
||||
if len(releases) > 0 {
|
||||
sort.Slice(releases, func(i, j int) bool {
|
||||
return releases[i].CreatedAt.After(releases[j].CreatedAt)
|
||||
})
|
||||
latest_release = &releases[0]
|
||||
}
|
||||
|
||||
var web_playable_release *Release
|
||||
for i := range releases {
|
||||
if releases[i].HTMLFolderPath != "" {
|
||||
web_playable_release = &releases[i] // fix: slice elem címe, nem a loop változóé
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &SoftwareShowData{
|
||||
Software: software,
|
||||
Releases: releases,
|
||||
LatestRelease: latest_release,
|
||||
WebPlayableRelease: web_playable_release,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SoftwareService) GetLatestRelease(software_id string) (*Release, error) {
|
||||
software, err := s.softeare_repository.GetByName(software_id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if software == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if len(software.Releases) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
sort.Slice(software.Releases, func(i, j int) bool {
|
||||
return software.Releases[i].CreatedAt.After(software.Releases[j].CreatedAt)
|
||||
})
|
||||
return &software.Releases[0], nil
|
||||
}
|
||||
|
||||
func (s *SoftwareService) DetailedList() (*SoftwareDetaildListDTO, error) {
|
||||
softwares, err := s.softeare_repository.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dtos := make([]SoftwareShowData, 0, len(softwares))
|
||||
for _, software := range softwares {
|
||||
showData, err := s.GetForShowByName(software.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dtos = append(dtos, *showData)
|
||||
}
|
||||
|
||||
return &SoftwareDetaildListDTO{Softwares: dtos}, nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterServiceInterface interface {
|
||||
Update(platform, name, version string) error
|
||||
}
|
||||
|
||||
type SoftwareUpdaterService struct {
|
||||
tic80Updater SoftwareUpdaterTIC80ServiceInterface
|
||||
ebitengineUpdater SoftwareUpdaterEbitengineServiceInterface
|
||||
loveUpdater SoftwareUpdaterLoveServiceInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterService(tic80_updater SoftwareUpdaterTIC80ServiceInterface, ebitengine_updater SoftwareUpdaterEbitengineServiceInterface, love_updater SoftwareUpdaterLoveServiceInterface) *SoftwareUpdaterService {
|
||||
return &SoftwareUpdaterService{
|
||||
tic80Updater: tic80_updater,
|
||||
ebitengineUpdater: ebitengine_updater,
|
||||
loveUpdater: love_updater,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterService) Update(platform, name, version string) error {
|
||||
if platform == "tic80" {
|
||||
return s.tic80Updater.Update(name, version)
|
||||
}
|
||||
if platform == "ebitengine" {
|
||||
return s.ebitengineUpdater.Update(name, version)
|
||||
}
|
||||
if platform == "love" {
|
||||
return s.loveUpdater.Update(name, version)
|
||||
}
|
||||
return fmt.Errorf("unsupported platform: %s", platform)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterEbitengineMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Desc string `json:"desc"`
|
||||
Site string `json:"site"`
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
type SoftwareUpdaterEbitengineServiceInterface interface {
|
||||
Update(name, version string) error
|
||||
}
|
||||
|
||||
type SoftwareUpdaterEbitengineService struct {
|
||||
softwareRepository SoftwareRepositoryInterface
|
||||
releaseRepository ReleaseRepositoryInterface
|
||||
fileRepository FileRepositoryInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterEbitengineService(
|
||||
software_repository SoftwareRepositoryInterface,
|
||||
release_repository ReleaseRepositoryInterface,
|
||||
file_repository FileRepositoryInterface,
|
||||
) *SoftwareUpdaterEbitengineService {
|
||||
return &SoftwareUpdaterEbitengineService{
|
||||
softwareRepository: software_repository,
|
||||
releaseRepository: release_repository,
|
||||
fileRepository: file_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterEbitengineService) Update(name, version string) error {
|
||||
fmt.Printf("Ebitengine Updater: Starting update for name: %s, version: %s\n", name, version)
|
||||
|
||||
versioned_name := name + "-" + version
|
||||
|
||||
zip_filename := versioned_name + ".html.zip"
|
||||
metadata_filename := versioned_name + ".metadata.json"
|
||||
html_dirname := versioned_name
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Processing zip file: %s\n", zip_filename)
|
||||
fmt.Printf("Ebitengine Updater: Processing metadata file: %s\n", metadata_filename)
|
||||
fmt.Printf("Ebitengine Updater: Processing HTML directory: %s\n", html_dirname)
|
||||
|
||||
if s.fileRepository.FileExists(html_dirname) {
|
||||
fmt.Printf("Ebitengine Updater: Removing existing HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.DeleteDir(html_dirname)
|
||||
}
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Creating HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.CreateDir(html_dirname)
|
||||
fmt.Printf("Ebitengine Updater: Unzipping file: %s to directory: %s\n", zip_filename, html_dirname)
|
||||
s.fileRepository.UnzipFile(zip_filename, html_dirname)
|
||||
|
||||
html_dir_path := s.fileRepository.GetPath(html_dirname)
|
||||
metadata_path := s.fileRepository.GetPath(metadata_filename)
|
||||
|
||||
var err error
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Extracting metadata from metadata file: %s\n", metadata_path)
|
||||
meta_data, err := s.GetMetadata(metadata_path)
|
||||
if err != nil {
|
||||
fmt.Printf("Ebitengine Updater: Error extracting metadata: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Ebitengine Updater: Extracted metadata: %+v\n", meta_data)
|
||||
software := s.BuildSoftware(meta_data)
|
||||
|
||||
err = s.softwareRepository.UpdateOrCreate(software)
|
||||
if err != nil {
|
||||
fmt.Printf("Ebitengine Updater: Error updating or creating software: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
release := Release{
|
||||
SoftwareID: software.ID,
|
||||
Version: version,
|
||||
CartridgePath: "",
|
||||
SourcePath: "",
|
||||
HTMLFolderPath: html_dir_path,
|
||||
}
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Creating release for software ID: %d, version: %s\n", software.ID, version)
|
||||
s.releaseRepository.CreateIfNotExist(&release)
|
||||
|
||||
fmt.Printf("Ebitengine Updater: Successfully processed Lua cartridge: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterEbitengineService) BuildSoftware(metadata SoftwareUpdaterEbitengineMetadata) *Software {
|
||||
software := &Software{
|
||||
Name: metadata.Name,
|
||||
Title: metadata.Title,
|
||||
Author: metadata.Author,
|
||||
Desc: metadata.Desc,
|
||||
Site: metadata.Site,
|
||||
License: metadata.License,
|
||||
Platform: "ebitengine",
|
||||
}
|
||||
|
||||
return software
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterEbitengineService) GetMetadata(metadata_path string) (SoftwareUpdaterEbitengineMetadata, error) {
|
||||
var metadata SoftwareUpdaterEbitengineMetadata
|
||||
|
||||
data, err := os.ReadFile(metadata_path)
|
||||
if err != nil {
|
||||
return metadata, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||
return metadata, err
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterLoveMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Desc string `json:"desc"`
|
||||
Site string `json:"site"`
|
||||
License string `json:"license"`
|
||||
}
|
||||
|
||||
type SoftwareUpdaterLoveServiceInterface interface {
|
||||
Update(name, version string) error
|
||||
}
|
||||
|
||||
type SoftwareUpdaterLoveService struct {
|
||||
softwareRepository SoftwareRepositoryInterface
|
||||
releaseRepository ReleaseRepositoryInterface
|
||||
fileRepository FileRepositoryInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterLoveService(
|
||||
software_repository SoftwareRepositoryInterface,
|
||||
release_repository ReleaseRepositoryInterface,
|
||||
file_repository FileRepositoryInterface,
|
||||
) *SoftwareUpdaterLoveService {
|
||||
return &SoftwareUpdaterLoveService{
|
||||
softwareRepository: software_repository,
|
||||
releaseRepository: release_repository,
|
||||
fileRepository: file_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterLoveService) Update(name, version string) error {
|
||||
fmt.Printf("Love Updater: Starting update for name: %s, version: %s\n", name, version)
|
||||
|
||||
versioned_name := name + "-" + version
|
||||
|
||||
zip_filename := versioned_name + ".html.zip"
|
||||
metadata_filename := versioned_name + ".metadata.json"
|
||||
html_dirname := versioned_name
|
||||
|
||||
fmt.Printf("Love Updater: Processing zip file: %s\n", zip_filename)
|
||||
fmt.Printf("Love Updater: Processing metadata file: %s\n", metadata_filename)
|
||||
fmt.Printf("Love Updater: Processing HTML directory: %s\n", html_dirname)
|
||||
|
||||
if s.fileRepository.FileExists(html_dirname) {
|
||||
fmt.Printf("Love Updater: Removing existing HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.DeleteDir(html_dirname)
|
||||
}
|
||||
|
||||
fmt.Printf("Love Updater: Creating HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.CreateDir(html_dirname)
|
||||
fmt.Printf("Love Updater: Unzipping file: %s to directory: %s\n", zip_filename, html_dirname)
|
||||
s.fileRepository.UnzipFile(zip_filename, html_dirname)
|
||||
|
||||
html_dir_path := s.fileRepository.GetPath(html_dirname)
|
||||
metadata_path := s.fileRepository.GetPath(metadata_filename)
|
||||
|
||||
var err error
|
||||
|
||||
fmt.Printf("Love Updater: Extracting metadata from metadata file: %s\n", metadata_path)
|
||||
meta_data, err := s.GetMetadata(metadata_path)
|
||||
if err != nil {
|
||||
fmt.Printf("Love Updater: Error extracting metadata: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Love Updater: Extracted metadata: %+v\n", meta_data)
|
||||
software := s.BuildSoftware(meta_data)
|
||||
|
||||
err = s.softwareRepository.UpdateOrCreate(software)
|
||||
if err != nil {
|
||||
fmt.Printf("Love Updater: Error updating or creating software: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
release := Release{
|
||||
SoftwareID: software.ID,
|
||||
Version: version,
|
||||
CartridgePath: "",
|
||||
SourcePath: "",
|
||||
HTMLFolderPath: html_dir_path,
|
||||
}
|
||||
|
||||
fmt.Printf("Love Updater: Creating release for software ID: %d, version: %s\n", software.ID, version)
|
||||
s.releaseRepository.CreateIfNotExist(&release)
|
||||
|
||||
fmt.Printf("Love Updater: Successfully processed Lua cartridge: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterLoveService) BuildSoftware(metadata SoftwareUpdaterLoveMetadata) *Software {
|
||||
software := &Software{
|
||||
Name: metadata.Name,
|
||||
Title: metadata.Title,
|
||||
Author: metadata.Author,
|
||||
Desc: metadata.Desc,
|
||||
Site: metadata.Site,
|
||||
License: metadata.License,
|
||||
Platform: "love",
|
||||
}
|
||||
|
||||
return software
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterLoveService) GetMetadata(metadata_path string) (SoftwareUpdaterLoveMetadata, error) {
|
||||
var metadata SoftwareUpdaterLoveMetadata
|
||||
|
||||
data, err := os.ReadFile(metadata_path)
|
||||
if err != nil {
|
||||
return metadata, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &metadata); err != nil {
|
||||
return metadata, err
|
||||
}
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterTIC80ServiceInterface interface {
|
||||
Update(name, version string) error
|
||||
}
|
||||
|
||||
type SoftwareUpdaterTIC80Service struct {
|
||||
softwareRepository SoftwareRepositoryInterface
|
||||
releaseRepository ReleaseRepositoryInterface
|
||||
fileRepository FileRepositoryInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterTIC80Service(
|
||||
software_repository SoftwareRepositoryInterface,
|
||||
release_repository ReleaseRepositoryInterface,
|
||||
file_repository FileRepositoryInterface,
|
||||
) *SoftwareUpdaterTIC80Service {
|
||||
return &SoftwareUpdaterTIC80Service{
|
||||
softwareRepository: software_repository,
|
||||
releaseRepository: release_repository,
|
||||
fileRepository: file_repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterTIC80Service) Update(name, version string) error {
|
||||
fmt.Printf("TIC80 Updater: Starting update for name: %s, version: %s\n", name, version)
|
||||
|
||||
versioned_name := name + "-" + version
|
||||
|
||||
zip_filename := versioned_name + ".html.zip"
|
||||
docs_zip_filename := versioned_name + "-docs.zip"
|
||||
cartridge_filename := versioned_name + ".tic"
|
||||
source_filename := versioned_name + ".lua"
|
||||
html_dirname := versioned_name
|
||||
docs_dirname := versioned_name + "-docs"
|
||||
|
||||
fmt.Printf("TIC80 Updater: Processing zip file: %s\n", zip_filename)
|
||||
fmt.Printf("TIC80 Updater: Processing docs zip file: %s\n", docs_zip_filename)
|
||||
fmt.Printf("TIC80 Updater: Processing cartridge file: %s\n", cartridge_filename)
|
||||
fmt.Printf("TIC80 Updater: Processing source file: %s\n", source_filename)
|
||||
fmt.Printf("TIC80 Updater: Processing HTML directory: %s\n", html_dirname)
|
||||
fmt.Printf("TIC80 Updater: Processing docs directory: %s\n", docs_dirname)
|
||||
|
||||
if s.fileRepository.FileExists(html_dirname) {
|
||||
fmt.Printf("TIC80 Updater: Removing existing HTML directory: %s\n", html_dirname)
|
||||
s.fileRepository.DeleteDir(html_dirname)
|
||||
}
|
||||
|
||||
if s.fileRepository.FileExists(docs_dirname) {
|
||||
fmt.Printf("TIC80 Updater: Removing existing docs directory: %s\n", docs_dirname)
|
||||
s.fileRepository.DeleteDir(docs_dirname)
|
||||
}
|
||||
|
||||
s.fileRepository.CreateDir(html_dirname)
|
||||
fmt.Printf("TIC80 Updater: Unzipping file: %s to directory: %s\n", zip_filename, html_dirname)
|
||||
s.fileRepository.UnzipFile(zip_filename, html_dirname)
|
||||
|
||||
s.fileRepository.CreateDir(docs_dirname)
|
||||
fmt.Printf("TIC80 Updater: Unzipping docs file: %s to directory: %s\n", docs_zip_filename, docs_dirname)
|
||||
s.fileRepository.UnzipFile(docs_zip_filename, docs_dirname)
|
||||
|
||||
cartridge_path := s.fileRepository.GetPath((cartridge_filename))
|
||||
source_path := s.fileRepository.GetPath(source_filename)
|
||||
html_dir_path := s.fileRepository.GetPath(html_dirname)
|
||||
docs_dir_path := s.fileRepository.GetPath(docs_dirname)
|
||||
|
||||
var err error
|
||||
fmt.Printf("TIC80 Updater: Extracting metadata from source file: %s\n", source_path)
|
||||
meta_data, err := s.GetMetadata(source_path)
|
||||
if err != nil {
|
||||
fmt.Printf("TIC80 Updater: Error extracting metadata: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
software := s.BuildSoftware(meta_data)
|
||||
err = s.softwareRepository.UpdateOrCreate(software)
|
||||
if err != nil {
|
||||
fmt.Printf("TIC80 Updater: Error updating or creating software: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("TIC80 Updater: Creating release for software ID: %d, version: %s\n", software.ID, version)
|
||||
release := Release{
|
||||
SoftwareID: software.ID,
|
||||
Version: version,
|
||||
CartridgePath: cartridge_path,
|
||||
SourcePath: source_path,
|
||||
HTMLFolderPath: html_dir_path,
|
||||
DocsFolderPath: docs_dir_path,
|
||||
}
|
||||
|
||||
s.releaseRepository.CreateIfNotExist(&release)
|
||||
|
||||
fmt.Printf("TIC80 Updater: Successfully processed Lua cartridge: %s\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterTIC80Service) BuildSoftware(metadata map[string]string) *Software {
|
||||
software := &Software{
|
||||
Name: metadata["name"],
|
||||
Title: metadata["title"],
|
||||
Author: metadata["author"],
|
||||
Desc: metadata["desc"],
|
||||
Site: metadata["site"],
|
||||
License: metadata["license"],
|
||||
Platform: "tic80",
|
||||
}
|
||||
|
||||
return software
|
||||
}
|
||||
|
||||
func (s *SoftwareUpdaterTIC80Service) GetMetadata(source_path string) (map[string]string, error) {
|
||||
|
||||
file, err := os.Open(source_path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
meta_data := make(map[string]string)
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "--") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(line, "--"), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
meta_data[strings.ToLower(key)] = val
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return meta_data, nil
|
||||
}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
go mod download && air -c .air.toml
|
||||
@@ -0,0 +1,17 @@
|
||||
module teletype_softwares
|
||||
|
||||
go 1.21.5
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.1
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/google/uuid v1.4.0
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.1 h1:FK6RCIUSfmbnI/imIICmboyQBkOckutaa6R5YYlLZyo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.1/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
||||
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
||||
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
@@ -0,0 +1,47 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"teletype_softwares/domain"
|
||||
)
|
||||
|
||||
type APISoftwareController struct {
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
}
|
||||
|
||||
func NewAPISoftwareController(software_service domain.SoftwareServiceInterface) *APISoftwareController {
|
||||
return &APISoftwareController{softwareService: software_service}
|
||||
}
|
||||
|
||||
func replaceReleasePaths(release *domain.Release) {
|
||||
release.CartridgePath = strings.ReplaceAll(release.CartridgePath, "/softwares/", "/file/")
|
||||
release.SourcePath = strings.ReplaceAll(release.SourcePath, "/softwares/", "/file/")
|
||||
release.HTMLFolderPath = strings.ReplaceAll(release.HTMLFolderPath, "/softwares/", "/file/")
|
||||
release.DocsFolderPath = strings.ReplaceAll(release.DocsFolderPath, "/softwares/", "/file/")
|
||||
}
|
||||
|
||||
func (c *APISoftwareController) Index(w http.ResponseWriter, r *http.Request) {
|
||||
softwares, _ := c.softwareService.DetailedList()
|
||||
|
||||
for idx := range softwares.Softwares {
|
||||
softwareShowData := &softwares.Softwares[idx]
|
||||
|
||||
for i := range softwareShowData.Releases {
|
||||
replaceReleasePaths(&softwareShowData.Releases[i])
|
||||
}
|
||||
|
||||
// LatestRelease és WebPlayableRelease a Releases slice elemeire mutat,
|
||||
// ezért ezeket külön is frissíteni kell, mivel értékmásolat kerülhet bele
|
||||
if softwareShowData.LatestRelease != nil {
|
||||
replaceReleasePaths(softwareShowData.LatestRelease)
|
||||
}
|
||||
if softwareShowData.WebPlayableRelease != nil {
|
||||
replaceReleasePaths(softwareShowData.WebPlayableRelease)
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(softwares)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"teletype_softwares/domain"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type DocsController struct {
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
fileService domain.FileServiceInterface
|
||||
}
|
||||
|
||||
func NewDocsController(software_service domain.SoftwareServiceInterface, file_service domain.FileServiceInterface) *DocsController {
|
||||
return &DocsController{
|
||||
softwareService: software_service,
|
||||
fileService: file_service,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DocsController) ServeDocs(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
version := chi.URLParam(r, "version")
|
||||
|
||||
software, err := c.softwareService.GetByName(name)
|
||||
if err != nil {
|
||||
if err == domain.ErrSoftwareNotFound {
|
||||
http.Error(w, "Software not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var targetRelease *domain.Release
|
||||
for _, release := range software.Releases {
|
||||
if release.Version == version && release.DocsFolderPath != "" {
|
||||
targetRelease = &release
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetRelease == nil {
|
||||
http.Error(w, "No documentation found for this release", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
docs_base_dir := targetRelease.DocsFolderPath
|
||||
|
||||
if _, err := os.Stat(docs_base_dir); os.IsNotExist(err) {
|
||||
http.Error(w, fmt.Sprintf("Documentation for '%s' version '%s' not found.", name, version), http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Error accessing documentation for '%s' version '%s': %v", name, version, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
prefix := fmt.Sprintf("/docs/%s/%s", name, version)
|
||||
|
||||
if r.URL.Path == prefix {
|
||||
http.Redirect(w, r, prefix+"/", http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
|
||||
fs := http.StripPrefix(prefix, http.FileServer(http.Dir(docs_base_dir)))
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"teletype_softwares/domain"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type DownloadController struct {
|
||||
downloadService domain.DownloadServiceInterface
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
}
|
||||
|
||||
func NewDownloadController(
|
||||
downloadService domain.DownloadServiceInterface,
|
||||
softwareService domain.SoftwareServiceInterface,
|
||||
) *DownloadController {
|
||||
return &DownloadController{
|
||||
downloadService: downloadService,
|
||||
softwareService: softwareService,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DownloadController) serve(w http.ResponseWriter, r *http.Request, release *domain.Release, is_source bool) {
|
||||
var file_path string
|
||||
if is_source {
|
||||
file_path = release.SourcePath
|
||||
} else {
|
||||
file_path = release.CartridgePath
|
||||
}
|
||||
|
||||
abs_file_path, err := filepath.Abs(file_path)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid file path", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
abs_contents_dir, err := filepath.Abs(os.Getenv("FILE_CONTAINER_PATH"))
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid contents directory path", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(abs_file_path, abs_contents_dir) {
|
||||
http.Error(w, "Access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(file_path))
|
||||
http.ServeFile(w, r, file_path)
|
||||
}
|
||||
|
||||
func (c *DownloadController) handleError(w http.ResponseWriter, err error) {
|
||||
if err == domain.ErrSoftwareNotFound || err == domain.ErrNoReleasesFound {
|
||||
http.Error(w, "Software not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err == domain.ErrReleaseNotFound {
|
||||
http.Error(w, "Software or release not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (c *DownloadController) GetLatestSource(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
release, err := c.downloadService.GetLatestRelease(name)
|
||||
if err != nil {
|
||||
c.handleError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.serve(w, r, release, true)
|
||||
}
|
||||
|
||||
func (c *DownloadController) GetLatestCartridge(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
release, err := c.downloadService.GetLatestRelease(name)
|
||||
if err != nil {
|
||||
c.handleError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.serve(w, r, release, false)
|
||||
}
|
||||
|
||||
func (c *DownloadController) GetSource(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
version := chi.URLParam(r, "version")
|
||||
release, err := c.downloadService.GetSpecificRelease(name, version)
|
||||
if err != nil {
|
||||
c.handleError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.serve(w, r, release, true)
|
||||
}
|
||||
|
||||
func (c *DownloadController) GetCartridge(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
version := chi.URLParam(r, "version")
|
||||
release, err := c.downloadService.GetSpecificRelease(name, version)
|
||||
if err != nil {
|
||||
c.handleError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.serve(w, r, release, false)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"teletype_softwares/domain"
|
||||
"teletype_softwares/lib/template_utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type PlayController struct {
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
fileService domain.FileServiceInterface
|
||||
}
|
||||
|
||||
func NewPlayController(software_service domain.SoftwareServiceInterface, file_service domain.FileServiceInterface) *PlayController {
|
||||
return &PlayController{
|
||||
softwareService: software_service,
|
||||
fileService: file_service,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PlayController) Play(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
version := chi.URLParam(r, "version")
|
||||
|
||||
software, err := c.softwareService.GetByName(name)
|
||||
if err != nil {
|
||||
if err == domain.ErrSoftwareNotFound {
|
||||
http.Error(w, "Software not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var web_playable_release *domain.Release
|
||||
for _, release := range software.Releases {
|
||||
if release.Version == version && release.HTMLFolderPath != "" {
|
||||
web_playable_release = &release
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if web_playable_release == nil {
|
||||
http.Error(w, "No web-playable version found for this software", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
softwares, err := c.softwareService.List()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, err := template_utils.GetTemplate("play_controller_play", "http/views/shared/layout.html", "http/views/play/play.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl.Execute(w, map[string]interface{}{
|
||||
"Software": software,
|
||||
"WebPlayableRelease": web_playable_release,
|
||||
"Softwares": softwares,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *PlayController) ServeContent(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
version := chi.URLParam(r, "version")
|
||||
|
||||
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
|
||||
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
|
||||
|
||||
html_base_dir := c.fileService.GetPath(name + "-" + version)
|
||||
|
||||
if _, err := os.Stat(html_base_dir); os.IsNotExist(err) {
|
||||
http.Error(w, fmt.Sprintf("Content for '%s' not found.", name), http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Error accessing content for '%s': %v", name, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fs := http.StripPrefix(
|
||||
fmt.Sprintf("/play/%s/%s/content", name, version),
|
||||
http.FileServer(http.Dir(html_base_dir)),
|
||||
)
|
||||
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"teletype_softwares/domain"
|
||||
"teletype_softwares/lib/template_utils"
|
||||
)
|
||||
|
||||
type RootController struct {
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
}
|
||||
|
||||
func NewRootController(softwareService domain.SoftwareServiceInterface) *RootController {
|
||||
return &RootController{
|
||||
softwareService: softwareService,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RootController) Index(w http.ResponseWriter, r *http.Request) {
|
||||
softwares, err := c.softwareService.List()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Softwares": softwares,
|
||||
}
|
||||
|
||||
tmpl, err := template_utils.GetTemplate("root_index", "http/views/shared/layout.html", "http/views/root/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl.Execute(w, data)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"teletype_softwares/domain"
|
||||
"teletype_softwares/lib/template_utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type SoftwareController struct {
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
}
|
||||
|
||||
func NewSoftwareController(software_service domain.SoftwareServiceInterface) *SoftwareController {
|
||||
return &SoftwareController{softwareService: software_service}
|
||||
}
|
||||
|
||||
func (c *SoftwareController) Index(w http.ResponseWriter, r *http.Request) {
|
||||
softwares, err := c.softwareService.List()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Softwares": softwares,
|
||||
}
|
||||
|
||||
tmpl, err := template_utils.GetTemplate("software_index", "http/views/shared/layout.html", "http/views/software/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
func (c *SoftwareController) Show(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
|
||||
showData, err := c.softwareService.GetForShowByName(name)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
softwares, err := c.softwareService.List()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Software": showData.Software,
|
||||
"Releases": showData.Releases,
|
||||
"LatestRelease": showData.LatestRelease,
|
||||
"WebPlayableRelease": showData.WebPlayableRelease,
|
||||
"Softwares": softwares,
|
||||
}
|
||||
|
||||
tmpl, err := template_utils.GetTemplate("software_show", "http/views/shared/layout.html", "http/views/software/show.html")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tmpl.Execute(w, data)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"teletype_softwares/domain"
|
||||
)
|
||||
|
||||
type SoftwareUpdaterController struct {
|
||||
softwareUpdaterService domain.SoftwareUpdaterServiceInterface
|
||||
softwareService domain.SoftwareServiceInterface
|
||||
}
|
||||
|
||||
func NewSoftwareUpdaterController(
|
||||
softwareUpdaterService domain.SoftwareUpdaterServiceInterface,
|
||||
softwareService domain.SoftwareServiceInterface,
|
||||
) *SoftwareUpdaterController {
|
||||
return &SoftwareUpdaterController{
|
||||
softwareUpdaterService: softwareUpdaterService,
|
||||
softwareService: softwareService,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SoftwareUpdaterController) Update(w http.ResponseWriter, r *http.Request) {
|
||||
secret := r.URL.Query().Get("secret")
|
||||
if secret != os.Getenv("UPDATE_SECRET") {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
platform := r.URL.Query().Get("platform")
|
||||
name := r.URL.Query().Get("name")
|
||||
version := r.URL.Query().Get("version")
|
||||
|
||||
if version == "" {
|
||||
http.Error(w, "Version not provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.softwareUpdaterService.Update(platform, name, version); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Updated"))
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"teletype_softwares/domain"
|
||||
"teletype_softwares/lib/http_utils"
|
||||
)
|
||||
|
||||
func StartHttpServer(domain_instance domain.Domain) {
|
||||
router := NewRouter(
|
||||
NewAPISoftwareController(domain_instance.SoftwareService),
|
||||
NewSoftwareController(domain_instance.SoftwareService),
|
||||
NewSoftwareUpdaterController(domain_instance.SoftwareUpdaterService, domain_instance.SoftwareService),
|
||||
NewDownloadController(domain_instance.DownloadService, domain_instance.SoftwareService),
|
||||
NewPlayController(domain_instance.SoftwareService, domain_instance.FileService),
|
||||
NewRootController(domain_instance.SoftwareService),
|
||||
NewDocsController(domain_instance.SoftwareService, domain_instance.FileService),
|
||||
).Init()
|
||||
|
||||
http_utils.StartGenericHTTPServer(http_utils.StartGenericHTTPServerContext{
|
||||
Router: router,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
func CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
|
||||
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
apiSoftwareController *APISoftwareController
|
||||
softwareController *SoftwareController
|
||||
softwareUpdaterController *SoftwareUpdaterController
|
||||
downloadController *DownloadController
|
||||
playController *PlayController
|
||||
rootController *RootController
|
||||
docsController *DocsController
|
||||
}
|
||||
|
||||
func NewRouter(
|
||||
api_software_controller *APISoftwareController,
|
||||
software_controller *SoftwareController,
|
||||
software_updater_controller *SoftwareUpdaterController,
|
||||
download_controller *DownloadController,
|
||||
play_controller *PlayController,
|
||||
root_controller *RootController,
|
||||
docs_controller *DocsController,
|
||||
) *Router {
|
||||
return &Router{
|
||||
apiSoftwareController: api_software_controller,
|
||||
softwareController: software_controller,
|
||||
softwareUpdaterController: software_updater_controller,
|
||||
downloadController: download_controller,
|
||||
playController: play_controller,
|
||||
rootController: root_controller,
|
||||
docsController: docs_controller,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) Init() *chi.Mux {
|
||||
router := chi.NewRouter()
|
||||
router.Use(CORSMiddleware)
|
||||
|
||||
router.Get("/", r.rootController.Index)
|
||||
|
||||
router.Get("/api/software", r.apiSoftwareController.Index)
|
||||
|
||||
router.Get("/software", r.softwareController.Index)
|
||||
router.Get("/software/{name}", r.softwareController.Show)
|
||||
|
||||
router.Get("/update", r.softwareUpdaterController.Update)
|
||||
router.Get("/download/{name}/source", r.downloadController.GetLatestSource)
|
||||
router.Get("/download/{name}/cartridge", r.downloadController.GetLatestCartridge)
|
||||
router.Get("/download/{name}/{version}/source", r.downloadController.GetSource)
|
||||
router.Get("/download/{name}/{version}/cartridge", r.downloadController.GetCartridge)
|
||||
router.Get("/play/{name}/{version}", r.playController.Play)
|
||||
router.Get("/play/{name}/{version}/content*", r.playController.ServeContent)
|
||||
router.Get("/docs/{name}/{version}", r.docsController.ServeDocs)
|
||||
router.Get("/docs/{name}/{version}/*", r.docsController.ServeDocs)
|
||||
|
||||
fs_assets := http.FileServer(http.Dir("assets"))
|
||||
router.Handle("/assets/*", http.StripPrefix("/assets/", fs_assets))
|
||||
|
||||
fs_file := http.FileServer(http.Dir("/softwares"))
|
||||
router.Handle("/file/*", http.StripPrefix("/file/", fs_file))
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{{define "content"}}
|
||||
<h1 class="my-4">Play {{.Software.Title}}</h1>
|
||||
|
||||
<div class="ratio ratio-4x3" style="max-width: 960px; margin: 0 auto;">
|
||||
<iframe id="gameFrame" src="/play/{{.Software.Name}}/{{.WebPlayableRelease.Version}}/content/index.html" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 960px; margin: 10px auto; text-align: center;">
|
||||
<div class="btn-group" role="group" aria-label="Game controls">
|
||||
<a href="/releases/{{.Software.Name}}" class="btn btn-primary">Back to Releases</a>
|
||||
{{if .WebPlayableRelease.DocsFolderPath}}
|
||||
<a href="/docs/{{.Software.Name}}/{{.WebPlayableRelease.Version}}" class="btn btn-info">Docs</a>
|
||||
{{end}}
|
||||
<button id="fullscreenButton" class="btn btn-secondary">Fullscreen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 960px; margin: 10px auto; text-align: start;">
|
||||
<h2 class="my-4">Version</h2>
|
||||
<p>{{.WebPlayableRelease.Version}}</p>
|
||||
<h2 class="my-4">Other Versions</h2>
|
||||
<div class="list-group">
|
||||
{{range .Software.Releases}}
|
||||
{{if and .WebPlayable (ne .Version $.WebPlayableRelease.Version)}}
|
||||
<a href="/play/{{$.Software.Name}}/{{.Version}}" class="list-group-item list-group-item-action">
|
||||
{{.Version}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('fullscreenButton').addEventListener('click', function() {
|
||||
var iframe = document.getElementById('gameFrame');
|
||||
if (iframe.requestFullscreen) {
|
||||
iframe.requestFullscreen();
|
||||
} else if (iframe.mozRequestFullScreen) { /* Firefox */
|
||||
iframe.mozRequestFullScreen();
|
||||
} else if (iframe.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
|
||||
iframe.webkitRequestFullscreen();
|
||||
} else if (iframe.msRequestFullscreen) { /* IE/Edge */
|
||||
iframe.msRequestFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('fullscreenchange', function() {
|
||||
if (document.fullscreenElement) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
document.addEventListener('mozfullscreenchange', function() {
|
||||
if (document.mozFullScreenElement) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
document.addEventListener('webkitfullscreenchange', function() {
|
||||
if (document.webkitFullscreenElement) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
document.addEventListener('msfullscreenchange', function() {
|
||||
if (document.msFullscreenElement) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{end}}
|
||||
@@ -0,0 +1,8 @@
|
||||
{{define "content"}}
|
||||
<h1 class="my-4">Welcome</h1>
|
||||
|
||||
<p>
|
||||
Welcome to Teletype Games! We are an independent, community-driven game development collective. Our mission is to create small, experimental, and full-fledged games in short development cycles while keeping everything open, transparent, and collaborative.
|
||||
</p>
|
||||
|
||||
{{end}}
|
||||
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Teletype Games</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<link href="/assets/css/style.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header class="navbar navbar-expand-lg header-border">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/" >Teletype Games</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://www.youtube.com/@teletypegames" target="_blank">Youtube</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://git.teletype.hu/explore/repos" target="_blank">Repositories</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://wiki.teletype.hu/" target="_blank">Wiki</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container mt-4 mb-4">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="content-border">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item disabled" aria-disabled="true">Softwares</li>
|
||||
{{range .Softwares}}
|
||||
<li class="list-group-item">
|
||||
<a href="/software/{{.Software.Name}}">{{.Software.Name}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="content-border p-3">
|
||||
{{block "content" .}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer-border text-center py-3">
|
||||
<p>
|
||||
<a class="nav-link" href="http://teletype.hu" target="_blank">
|
||||
Teletype Games 2025-2026
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{{define "content"}}
|
||||
<h1 class="my-4">Softwares</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<ul>
|
||||
{{range .Softwares}}
|
||||
<li>
|
||||
<a href="/software/{{.Software.Name}}">{{.Software.Title}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,100 @@
|
||||
{{define "content"}}
|
||||
<h1 class="my-4">{{.Software.Title}}</h1>
|
||||
|
||||
<div class="row">
|
||||
{{if .WebPlayableRelease}}
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3>Play</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="ratio ratio-4x3" style="max-width: 960px; margin: 0 auto;">
|
||||
<iframe id="gameFrame" src="/play/{{.Software.Name}}/{{.WebPlayableRelease.Version}}/content/index.html"
|
||||
frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Game controls">
|
||||
<button id="fullscreenButton" class="btn btn-secondary btn-sm">Fullscreen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3>Overview</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" style="width: 30%;">Title</th>
|
||||
<td>{{.Software.Title}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Author</th>
|
||||
<td>{{.Software.Author}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{.Software.Desc}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Version</th>
|
||||
<td>{{.LatestRelease.Version}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">License</th>
|
||||
<td>{{.Software.License}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Website</th>
|
||||
<td><a href="{{.Software.Site}}" target="_blank">{{.Software.Site}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="btn-group d-flex justify-content-end" role="group" aria-label="Operations">
|
||||
<a href="/download/{{.Software.Name}}/source" class="btn btn-primary btn-sm">Source</a>
|
||||
<a href="/download/{{.Software.Name}}/cartridge" class="btn btn-primary btn-sm">Cartridge</a>
|
||||
{{if .WebPlayableRelease.DocsFolderPath}}
|
||||
<a href="/docs/{{.Software.Name}}/{{.WebPlayableRelease.Version}}" class="btn btn-primary btn-sm">Docs</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Releases</h3>
|
||||
<div class="row">
|
||||
{{range .Releases}}
|
||||
<div class="col-12 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Version: {{.Version}}</h5>
|
||||
<p class="card-subtitle mb-2 text-muted">Released At: {{.CreatedAt.Format "2006-01-02 15:04:05"}}</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Operations">
|
||||
{{if .HTMLFolderPath}}
|
||||
<a href="/play/{{$.Software.Name}}/{{.Version}}" class="btn btn-primary btn-sm">Play</a>
|
||||
{{end}}
|
||||
<a href="/download/{{$.Software.Name}}/{{.Version}}/source"
|
||||
class="btn btn-primary btn-sm">Source</a>
|
||||
<a href="/download/{{$.Software.Name}}/{{.Version}}/cartridge"
|
||||
class="btn btn-primary btn-sm">Cartridge</a>
|
||||
{{if .DocsFolderPath}}
|
||||
<a href="/docs/{{$.Software.Name}}/{{.Version}}" class="btn btn-primary btn-sm">Docs</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
package http_utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
|
||||
response, _ := json.Marshal(payload)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
w.Write(response)
|
||||
}
|
||||
|
||||
func GetParam(r *http.Request, name string) string {
|
||||
return chi.URLParam(r, name)
|
||||
}
|
||||
|
||||
func GetParamUUID(r *http.Request, name string) (uuid.UUID, error) {
|
||||
return uuid.Parse(GetParam(r, name))
|
||||
}
|
||||
|
||||
type StartGenericHTTPServerContext struct {
|
||||
Router *chi.Mux
|
||||
}
|
||||
|
||||
func StartGenericHTTPServer(ctx StartGenericHTTPServerContext) {
|
||||
r := chi.NewRouter()
|
||||
r.Use(LoggingMiddleware)
|
||||
|
||||
r.Mount("/", ctx.Router)
|
||||
http.Handle("/", r)
|
||||
log.Fatal(http.ListenAndServe(":80", r))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package http_utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("%s %s\n", r.Method, r.RequestURI)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
package mysql_utils
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DBMockSuite struct {
|
||||
DB *gorm.DB
|
||||
Mock sqlmock.Sqlmock
|
||||
}
|
||||
|
||||
func NewDBMockSuite() *DBMockSuite {
|
||||
var (
|
||||
db *sql.DB
|
||||
gormdb *gorm.DB
|
||||
mock sqlmock.Sqlmock
|
||||
err error
|
||||
)
|
||||
|
||||
db, mock, err = sqlmock.New()
|
||||
|
||||
if err != nil {
|
||||
panic("SQL Mock error")
|
||||
}
|
||||
|
||||
mock.ExpectQuery("SELECT VERSION()").WithArgs().WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
|
||||
|
||||
gormdb, err = gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: "sqlmock_db_0",
|
||||
DriverName: "mysql",
|
||||
Conn: db,
|
||||
}), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
panic("GORM open error")
|
||||
}
|
||||
|
||||
return &DBMockSuite{
|
||||
DB: gormdb,
|
||||
Mock: mock,
|
||||
}
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
package mysql_utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DbConnectData struct {
|
||||
User string
|
||||
Password string
|
||||
Host string
|
||||
Port string
|
||||
Database string
|
||||
}
|
||||
|
||||
func getDbConnectData() DbConnectData {
|
||||
return DbConnectData{
|
||||
User: os.Getenv("DB_USER"),
|
||||
Password: os.Getenv("DB_PASSWORD"),
|
||||
Host: os.Getenv("DB_HOST"),
|
||||
Port: os.Getenv("DB_PORT"),
|
||||
Database: os.Getenv("DB_NAME"),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Init() *gorm.DB {
|
||||
c := getDbConnectData()
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", c.User, c.Password, c.Host, c.Port, c.Database)
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package template_utils
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type TemplateCache struct {
|
||||
cache map[string]*template.Template
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var globalCache *TemplateCache
|
||||
|
||||
func init() {
|
||||
globalCache = &TemplateCache{
|
||||
cache: make(map[string]*template.Template),
|
||||
}
|
||||
}
|
||||
|
||||
func GetTemplate(name string, files ...string) (*template.Template, error) {
|
||||
globalCache.mu.RLock()
|
||||
if tmpl, exists := globalCache.cache[name]; exists {
|
||||
globalCache.mu.RUnlock()
|
||||
return tmpl, nil
|
||||
}
|
||||
globalCache.mu.RUnlock()
|
||||
|
||||
tmpl, err := template.ParseFiles(files...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
globalCache.mu.Lock()
|
||||
globalCache.cache[name] = tmpl
|
||||
globalCache.mu.Unlock()
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func ClearCache() {
|
||||
globalCache.mu.Lock()
|
||||
globalCache.cache = make(map[string]*template.Template)
|
||||
globalCache.mu.Unlock()
|
||||
}
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"teletype_softwares/domain"
|
||||
"teletype_softwares/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
domain := domain.NewDomain()
|
||||
http.StartHttpServer(domain)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
@@ -0,0 +1,46 @@
|
||||
# Astro Starter Kit: Basics
|
||||
|
||||
```sh
|
||||
npm create astro@latest -- --template basics
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```text
|
||||
/
|
||||
├── public/
|
||||
│ └── favicon.svg
|
||||
├── src
|
||||
│ ├── assets
|
||||
│ │ └── astro.svg
|
||||
│ ├── components
|
||||
│ │ └── Welcome.astro
|
||||
│ ├── layouts
|
||||
│ │ └── Layout.astro
|
||||
│ └── pages
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
@@ -0,0 +1,7 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [tailwind()],
|
||||
});
|
||||
Generated
+6477
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "ttglanding",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"astro": "^5.17.1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 655 B |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 749 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,210 @@
|
||||
---
|
||||
import astroLogo from '../assets/astro.svg';
|
||||
import background from '../assets/background.svg';
|
||||
---
|
||||
|
||||
<div id="container">
|
||||
<img id="background" src={background.src} alt="" fetchpriority="high" />
|
||||
<main>
|
||||
<section id="hero">
|
||||
<a href="https://astro.build"
|
||||
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
|
||||
>
|
||||
<h1>
|
||||
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
|
||||
</h1>
|
||||
<section id="links">
|
||||
<a class="button" href="https://docs.astro.build">Read our docs</a>
|
||||
<a href="https://astro.build/chat"
|
||||
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
|
||||
></path></svg
|
||||
>
|
||||
</a>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
|
||||
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
|
||||
fill="#111827"></path></svg
|
||||
>
|
||||
<h2>What's New in Astro 5.0?</h2>
|
||||
<p>
|
||||
From content layers to server islands, click to learn more about the new features and
|
||||
improvements in Astro 5.0
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
filter: blur(100px);
|
||||
}
|
||||
|
||||
#container {
|
||||
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#hero {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
#links {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#links a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
color: #111827;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
#links a:hover {
|
||||
color: rgb(78, 80, 86);
|
||||
}
|
||||
|
||||
#links a svg {
|
||||
height: 1em;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
#links a.button {
|
||||
color: white;
|
||||
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#links a.button:hover {
|
||||
color: rgb(230, 230, 230);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family:
|
||||
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
|
||||
monospace;
|
||||
font-weight: normal;
|
||||
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 1em;
|
||||
font-weight: normal;
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4b5563;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.006em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
background:
|
||||
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
|
||||
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
border-radius: 16px;
|
||||
border: 1px solid white;
|
||||
}
|
||||
|
||||
#news {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
max-width: 300px;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
backdrop-filter: blur(50px);
|
||||
}
|
||||
|
||||
#news:hover {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
@media screen and (max-height: 368px) {
|
||||
#news {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#hero {
|
||||
display: block;
|
||||
padding-top: 10%;
|
||||
}
|
||||
|
||||
#links {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#links a.button {
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
#news {
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
bottom: 2.5rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro Basics</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
const API = "https://games.teletype.hu/api/software";
|
||||
const BASE = "https://games.teletype.hu";
|
||||
|
||||
const res = await fetch(API);
|
||||
const json = await res.json();
|
||||
const softwares = json.softwares;
|
||||
---
|
||||
|
||||
<html lang="hu" class="bg-gray-50">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Teletype Games</title>
|
||||
</head>
|
||||
<body class="font-sans text-gray-800">
|
||||
<header class="text-center py-16 bg-gradient-to-r from-purple-600 to-indigo-600 text-white">
|
||||
<h1 class="text-5xl font-bold mb-4">Teletype Games</h1>
|
||||
<p class="text-xl max-w-xl mx-auto">Fedezd fel a TIC-80 és más platformokra készült játékokat, közvetlenül a böngészőből játszható verziókkal!</p>
|
||||
</header>
|
||||
|
||||
<main class="max-w-6xl mx-auto py-12 px-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{softwares.map(({ software, webPlayableRelease }) => (
|
||||
<div class="bg-white shadow-lg rounded-xl overflow-hidden hover:shadow-2xl transition-shadow duration-300">
|
||||
<div class="p-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">{software.title}</h2>
|
||||
<p class="text-gray-600 mb-4">{software.desc}</p>
|
||||
<div class="text-sm text-gray-500 mb-4">
|
||||
<div>Author: {software.author}</div>
|
||||
<div>Platform: {software.platform}</div>
|
||||
</div>
|
||||
{webPlayableRelease?.htmlFolderPath && (
|
||||
<a href={BASE + webPlayableRelease.htmlFolderPath} target="_blank"
|
||||
class="inline-block bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors">
|
||||
▶ Play in Browser
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</main>
|
||||
|
||||
<footer class="text-center py-8 text-gray-500 text-sm">
|
||||
© 2026 Teletype Games
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{astro,html,js,ts,jsx,tsx,vue,svelte}"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user