Commit 2dfeaae772

2dfeaae772e630ae361a35bf4680257d081560c1

parent: e02174cb71

Unsigned

lost+skunk <skunky@ebloid.ru> · 2024-07-29 22:06 UTC

Подготовка к релизу v1.3.1

Layout: unified · split

README.md +22 −17
@@ -1,11 +1,14 @@
1<img src="https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/misc/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%"/>
2
1[![Matrix room](https://img.shields.io/badge/matrix-000000?style=for-the-badge&logo=Matrix&logoColor=white)](https://go.kde.org/matrix/#/#skunkyart:ebloid.ru) 3[![Matrix room](https://img.shields.io/badge/matrix-000000?style=for-the-badge&logo=Matrix&logoColor=white)](https://go.kde.org/matrix/#/#skunkyart:ebloid.ru)
2# Instances 4# Instances
3|Инстанс|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country| 5|Instance|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country|
4|:-----:|:-------:|:-:|:-:|:--:|:--------:|:-----:| 6|:------:|:-------:|:-:|:-:|:--:|:--------:|:-----:|
5|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia | 7|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia |
6|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden | 8|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden |
7|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania | 9|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania |
8|[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland | 10|[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland |
11|[lumaeris.com](https://skunkyart.lumaeris.com)|No|No|No| Yes | Yes | US |
9 12
10# EN 🇺🇸 13# EN 🇺🇸
11## Description 14## Description
@@ -13,12 +16,12 @@ SkunkyArt 🦨 -- alternative frontend to DeviantArt, which will work without pr
13## Config 16## Config
14The sample config is in the `config.example.json` file. To specify your own path to the config, use the CLI argument `-c` or `--config`. 17The sample config is in the `config.example.json` file. To specify your own path to the config, use the CLI argument `-c` or `--config`.
15* `listen` -- the address and port on which SkunkyArt will listen 18* `listen` -- the address and port on which SkunkyArt will listen
16* `base-path` -- the path to the instance. Example: "`base-path`:"/art/" -> https://skunky.ebloid.ru/art/ 19* `base-path` -- the path to the instance. Example: `"base-path":"/art/"` -> https://skunky.ebloid.ru/art/
17* `cache` -- caching system; default is off. 20* `cache` -- caching system; default is off.
18* * `path` -- the path to the cache 21 * `path` -- the path to the cache
19* * `lifetime` -- cache file lifetime; measured in Unix milliseconds. 22 * `lifetime` -- the lifetime of the file in the cache. Units: i, h, d, w, m, y. I -- minute, all other units I think are self-explanatory.
20* * `max-size` -- maximum file size in bytes. 23 * `max-size` -- maximum file size in megabytes.
21* `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Required 24* `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Mandatory
22* `download-proxy` -- proxy address for downloading files. 25* `download-proxy` -- proxy address for downloading files.
23## Examples of reverse proxies 26## Examples of reverse proxies
24Nginx: 27Nginx:
@@ -27,17 +30,18 @@ server {
27 listen 443 ssl; 30 listen 443 ssl;
28 server_name skunky.example.com; 31 server_name skunky.example.com;
29 32
30 location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL))'. 33 location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL)))'.
31 proxy_set_header Scheme $scheme; 34 proxy_set_header X-Forwarded-Proto $scheme;
32 proxy_set_header Host $host; 35 proxy_set_header Host $host;
33 proxy_http_version 1.1; 36 proxy_http_version 1.1;
34 proxy_pass http://((IP)):((PORT)); 37 proxy_pass http://((IP)):((PORT));
35 } 38 }
36} 39}
37``` 40```
41Pretty much business as usual, except for the [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) header setting.
38## How do I add my instance to the list? 42## How do I add my instance to the list?
39To do this, you must either make a PR by adding your instance to the `instances.json` file, or report it to the room in Matrix. I don't think it needs any description. However, be aware, this list has a couple rules: 43To do this, you must either make a PR by adding your instance to the `instances.json` and `README.md` files, or create an Issue, or report it to the room in Matrix. I don't think it needs any description. However, be warned, this list has a couple rules:
401. the instance must not use Cloudflare. 441. the Instance must not use Cloudflare.
412. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not. 452. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not.
42## Acknowledgements 46## Acknowledgements
43* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- helped me understand Go and gave me a lot of useful advice on this language. 47* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- helped me understand Go and gave me a lot of useful advice on this language.
@@ -48,11 +52,11 @@ SkunkyArt 🦨 -- альтернативный фронтенд к DeviantArt,
48## Конфиг 52## Конфиг
49Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`. 53Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`.
50* `listen` -- адрес и порт, на котором будет слушать SkunkyArt 54* `listen` -- адрес и порт, на котором будет слушать SkunkyArt
51* `base-path` -- путь к инстансу. Пример: "base-path": "/art/" -> https://skunky.ebloid.ru/art/ 55* `base-path` -- путь к инстансу. Пример: `"base-path": "/art/"` -> https://skunky.ebloid.ru/art/
52* `cache` -- система кеширования; по умолчанию - выкл. 56* `cache` -- система кеширования; по умолчанию - выкл.
53* * `path` -- путь до кеша 57 * `path` -- путь до кеша
54* * `lifetime` -- время жизни файла в кеше; измеряется в Unix-миллисекундах 58 * `lifetime` -- время жизни файла в кеше. Единицы измерения: i, h, d, w, m, y. I -- минута, всё остальные единицы измерения, я считаю понятными и без объяснения.
55* * `max-size` -- максимальный размер файла в байтах 59 * `max-size` -- максимальный размер файла в мегабайтах
56* `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна 60* `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна
57* `download-proxy` -- адрес прокси для загрузки файлов 61* `download-proxy` -- адрес прокси для загрузки файлов
58## Примеры reverse-прокси 62## Примеры reverse-прокси
@@ -63,15 +67,16 @@ server {
63 server_name skunky.example.com; 67 server_name skunky.example.com;
64 68
65 location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/' 69 location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/'
66 proxy_set_header Scheme $scheme; 70 proxy_set_header X-Forwarded-Proto $scheme;
67 proxy_set_header Host $host; 71 proxy_set_header Host $host;
68 proxy_http_version 1.1; 72 proxy_http_version 1.1;
69 proxy_pass http://((IP)):((PORT)); 73 proxy_pass http://((IP)):((PORT));
70 } 74 }
71} 75}
72``` 76```
77В целом, всё как обычно, за исключением настройки заголовка [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto).
73## Как добавить свой инстанс в список? 78## Как добавить свой инстанс в список?
74Чтобы это сделать, вы должны либо сделать PR, добавив в файл `instances.json` свой инстанс, либо сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил: 79Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `README.md` свой инстанс, либо создать Issue, или сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил:
751. Инстанс не должен использовать Cloudflare. 801. Инстанс не должен использовать Cloudflare.
762. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются. 812. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются.
77## Благодарности 82## Благодарности
TODO.md +13 −4
@@ -1,7 +1,16 @@
1# v1.3.x 1# v1.3.x
2* Доделать парсинг описания 2* Написать Makefile
3* Реализовать миниатюры и оптимизировать CSS под маленькие экраны 3* Почистить говнокод
4* **Доделать парсинг описания**
5* Избавиться от хардкода под Linux
6* ~~Реализовать стрипы в ежедневных артах~~
7* ~~Исправить баг с навигацией по страницам~~
8* ~~Сделать единицы в конфиге более понятными~~
9* Добавить возможность включить темплейты в бинарник
10* ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~
11* **Реализовать отображение контента, отличного от картинок (видео, аудио, etc)**
12* Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ
4# v1.4 13# v1.4
14* Реализовать API
5* Реализовать темы 15* Реализовать темы
6* Реализовать многоязычный интерфейс 16* Реализовать многоязычный интерфейс\ No newline at end of file
7* Реализовать API\ No newline at end of file
app/config.go +53 −16
@@ -3,14 +3,18 @@ package app
3import ( 3import (
4 "encoding/json" 4 "encoding/json"
5 "os" 5 "os"
6 "regexp"
7 "strconv"
6 "time" 8 "time"
9
10 "git.macaw.me/skunky/devianter"
7) 11)
8 12
9type cache_config struct { 13type cache_config struct {
10 Enabled bool 14 Enabled bool
11 Path string 15 Path string
12 MaxSize int64 `json:"max-size"` 16 MaxSize int64 `json:"max-size"`
13 Lifetime int64 17 Lifetime string
14 UpdateInterval int64 `json:"update-interval"` 18 UpdateInterval int64 `json:"update-interval"`
15} 19}
16 20
@@ -20,6 +24,7 @@ type config struct {
20 BasePath string `json:"base-path"` 24 BasePath string `json:"base-path"`
21 Cache cache_config 25 Cache cache_config
22 Proxy, Nsfw bool 26 Proxy, Nsfw bool
27 UserAgent string `json:"user-agent"`
23 DownloadProxy string `json:"download-proxy"` 28 DownloadProxy string `json:"download-proxy"`
24 Dirs []string `json:"dirs-to-memory"` 29 Dirs []string `json:"dirs-to-memory"`
25} 30}
@@ -33,31 +38,36 @@ var CFG = config{
33 Path: "cache", 38 Path: "cache",
34 UpdateInterval: 1, 39 UpdateInterval: 1,
35 }, 40 },
36 Dirs: []string{"html", "css"}, 41 Dirs: []string{"html", "css"},
37 Proxy: true, 42 UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
38 Nsfw: true, 43 Proxy: true,
44 Nsfw: true,
39} 45}
40 46
47var lifetimeParsed int64
48
41func ExecuteConfig() { 49func ExecuteConfig() {
42 go func() { 50 go func() {
43 defer func() {
44 if r := recover(); r != nil {
45 recover()
46 }
47 }()
48 for { 51 for {
49 Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body) 52 func() {
50 time.Sleep(1 * time.Second) 53 defer func() {
54 if r := recover(); r != nil {
55 recover()
56 }
57 }()
58 Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
59 }()
60 time.Sleep(1 * time.Hour)
51 } 61 }
52 }() 62 }()
53 63
54 const helpmsg = `SkunkyArt v1.3 [refactoring] 64 const helpmsg = `SkunkyArt v1.3.1 [CSS improvements for mobile and strips on Daily Deviations]
55Usage: 65Usage:
56 - [-c|--config] - path to config 66 - [-c|--config] - path to config
57 - [-h|--help] - returns this message 67 - [-h|--help] - returns this message
58Example: 68Example:
59 ./skunkyart -c config.json 69 ./skunkyart -c config.json
60Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3` 70Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3.1`
61 71
62 a := os.Args 72 a := os.Args
63 for n, x := range a { 73 for n, x := range a {
@@ -75,15 +85,42 @@ Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`
75 85
76 if CFG.cfg != "" { 86 if CFG.cfg != "" {
77 f, err := os.ReadFile(CFG.cfg) 87 f, err := os.ReadFile(CFG.cfg)
78 try_with_exitstatus(err, 1) 88 tryWithExitStatus(err, 1)
79 89
80 try_with_exitstatus(json.Unmarshal(f, &CFG), 1) 90 tryWithExitStatus(json.Unmarshal(f, &CFG), 1)
81 if CFG.Cache.Enabled && !CFG.Proxy { 91 if CFG.Cache.Enabled && !CFG.Proxy {
82 exit("Incompatible settings detected: cannot use caching media content without proxy", 1) 92 exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
83 } 93 }
84 94
85 if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 { 95 if CFG.Cache.Enabled {
96 if CFG.Cache.Lifetime != "" {
97 var duration int64
98 day := 24 * time.Hour.Milliseconds()
99 numstr := regexp.MustCompile("[0-9]+").FindAllString(CFG.Cache.Lifetime, -1)
100 num, _ := strconv.Atoi(numstr[len(numstr)-1])
101
102 switch unit := CFG.Cache.Lifetime[len(CFG.Cache.Lifetime)-1:]; unit {
103 case "i":
104 duration = time.Minute.Milliseconds()
105 case "h":
106 duration = time.Hour.Milliseconds()
107 case "d":
108 duration = day
109 case "w":
110 duration = day * 7
111 case "m":
112 duration = day * 30
113 case "y":
114 duration = day * 360
115 default:
116 exit("Invalid unit specified: "+unit, 1)
117 }
118
119 lifetimeParsed = duration * int64(num)
120 }
121 CFG.Cache.MaxSize /= 1024 ^ 2
86 go InitCacheSystem() 122 go InitCacheSystem()
87 } 123 }
124 devianter.UserAgent = CFG.UserAgent
88 } 125 }
89} 126}
app/parsers.go +110 −103
@@ -70,95 +70,100 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
70 return cmmts.String() 70 return cmmts.String()
71} 71}
72 72
73func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...DeviationList) string { 73func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, content ...DeviationList) string {
74 var list strings.Builder
75 if s.Atom && s.Page > 1 { 74 if s.Atom && s.Page > 1 {
76 s.ReturnHTTPError(400) 75 s.ReturnHTTPError(400)
77 return "" 76 return ""
78 } else if s.Atom {
79 list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
80 list.WriteString(`<title>`)
81 if s.Type == 0 {
82 list.WriteString("Daily Deviations")
83 } else if len(devs) != 0 {
84 list.WriteString(devs[0].Author.Username)
85 } else {
86 list.WriteString("SkunkyArt")
87 }
88 list.WriteString(`</title>`)
89
90 list.WriteString(`<link rel="alternate" href="`)
91 list.WriteString(Host)
92 list.WriteString(`"/>`)
93 } else {
94 list.WriteString(`<div class="content">`)
95 } 77 }
96 for _, data := range devs { 78
97 if !(data.NSFW && !CFG.Nsfw) { 79 var list, listContent strings.Builder
98 url := ParseMedia(data.Media) 80
99 if s.Atom { 81 for i, l := 0, len(devs); i < l; i++ {
82 data := &devs[i]
83 if preview, fullview := ParseMedia(data.Media, 320), ParseMedia(data.Media); !(data.NSFW && !CFG.Nsfw) {
84 if allowAtom && s.Atom {
100 id := strconv.Itoa(data.ID) 85 id := strconv.Itoa(data.ID)
101 list.WriteString(`<entry><author><name>`) 86 listContent.WriteString(`<entry><author><name>`)
102 list.WriteString(data.Author.Username) 87 listContent.WriteString(data.Author.Username)
103 list.WriteString(`</name></author><title>`) 88 listContent.WriteString(`</name></author><title>`)
104 list.WriteString(data.Title) 89 listContent.WriteString(data.Title)
105 list.WriteString(`</title><link rel="alternate" type="text/html" href="`) 90 listContent.WriteString(`</title><link rel="alternate" type="text/html" href="`)
106 list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id)) 91 listContent.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
107 list.WriteString(`"/><id>`) 92 listContent.WriteString(`"/><id>`)
108 list.WriteString(id) 93 listContent.WriteString(id)
109 list.WriteString(`</id><published>`) 94 listContent.WriteString(`</id><published>`)
110 list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700")) 95 listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
111 list.WriteString(`</published>`) 96 listContent.WriteString(`</published>`)
112 list.WriteString(`<media:group><media:title>`) 97 listContent.WriteString(`<media:group><media:title>`)
113 list.WriteString(data.Title) 98 listContent.WriteString(data.Title)
114 list.WriteString(`</media:title><media:thumbinal url="`) 99 listContent.WriteString(`</media:title><media:thumbinal url="`)
115 list.WriteString(url) 100 listContent.WriteString(preview)
116 list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`) 101 listContent.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
117 list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url)) 102 listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
118 list.WriteString(`"><img src="`) 103 listContent.WriteString(`"><img src="`)
119 list.WriteString(url) 104 listContent.WriteString(fullview)
120 list.WriteString(`"/></a><p>`) 105 listContent.WriteString(`"/></a><p>`)
121 list.WriteString(ParseDescription(data.TextContent)) 106 listContent.WriteString(ParseDescription(data.TextContent))
122 list.WriteString(`</p></div></content></entry>`) 107 listContent.WriteString(`</p></div></content></entry>`)
123 } else { 108 } else {
124 list.WriteString(`<div class="block">`) 109 listContent.WriteString(`<div class="block">`)
125 if url != "" { 110 if fullview != "" && preview != "" {
126 list.WriteString(`<a title="open/download" href="`) 111 listContent.WriteString(`<a title="open/download" href="`)
127 list.WriteString(url) 112 listContent.WriteString(fullview)
128 list.WriteString(`"><img loading="lazy" src="`) 113 listContent.WriteString(`"><img loading="lazy" src="`)
129 list.WriteString(url) 114 listContent.WriteString(preview)
130 list.WriteString(`" width="15%"></a>`) 115 listContent.WriteString(`" width="15%"></a>`)
131 } else { 116 } else {
132 list.WriteString(`<h1>[ TEXT ]</h1>`) 117 listContent.WriteString(`<h1>[ TEXT ]</h1>`)
133 } 118 }
134 list.WriteString(`<br><a href="`) 119 listContent.WriteString(`<br><a href="`)
135 list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url)) 120 listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
136 list.WriteString(`">`) 121 listContent.WriteString(`">`)
137 list.WriteString(data.Author.Username) 122 listContent.WriteString(data.Author.Username)
138 list.WriteString(" - ") 123 listContent.WriteString(" - ")
139 list.WriteString(data.Title) 124 listContent.WriteString(data.Title)
140 125
141 // шильдики нсфв, аи и ежедневного поста
142 if data.NSFW { 126 if data.NSFW {
143 list.WriteString(` [<span class="nsfw">NSFW</span>]`) 127 listContent.WriteString(` [<span class="nsfw">NSFW</span>]`)
144 } 128 }
145 if data.AI { 129 if data.AI {
146 list.WriteString(" [🤖]") 130 listContent.WriteString(" [🤖]")
147 } 131 }
148 if data.DD { 132 if data.DD {
149 list.WriteString(` [<span class="dd">DD</span>]`) 133 listContent.WriteString(` [<span class="dd">DD</span>]`)
150 } 134 }
151 135
152 list.WriteString("</a></div>") 136 listContent.WriteString("</a></div>")
153 } 137 }
154 } 138 }
155 } 139 }
156 140
157 if s.Atom { 141 if allowAtom && s.Atom {
142 list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
143
144 list.WriteString(`<title>`)
145 if s.Type == 0 {
146 list.WriteString("Daily Deviations")
147 } else if s.Type == 'g' && len(devs) != 0 {
148 list.WriteString(devs[0].Author.Username)
149 } else {
150 list.WriteString("SkunkyArt")
151 }
152 list.WriteString(`</title>`)
153
154 list.WriteString(`<link rel="alternate" href="`)
155 list.WriteString(Host)
156 list.WriteString(`"/>`)
157
158 list.WriteString(listContent.String())
159
158 list.WriteString("</feed>") 160 list.WriteString("</feed>")
159 s.Writer.Write([]byte(list.String())) 161 wr(s.Writer, list.String())
160 return ""
161 } else { 162 } else {
163 list.WriteString(`<div class="content">`)
164
165 list.WriteString(listContent.String())
166
162 list.WriteString("</div>") 167 list.WriteString("</div>")
163 if content != nil { 168 if content != nil {
164 list.WriteString(s.NavBase(content[0])) 169 list.WriteString(s.NavBase(content[0]))
@@ -177,7 +182,7 @@ type text struct {
177} 182}
178 183
179func ParseDescription(dscr devianter.Text) string { 184func ParseDescription(dscr devianter.Text) string {
180 var parseddescription strings.Builder 185 var parsedDescription strings.Builder
181 TagBuilder := func(content string, tags ...string) string { 186 TagBuilder := func(content string, tags ...string) string {
182 l := len(tags) 187 l := len(tags)
183 for x := 0; x < l; x++ { 188 for x := 0; x < l; x++ {
@@ -274,16 +279,18 @@ func ParseDescription(dscr devianter.Text) string {
274 279
275 switch x.Type { 280 switch x.Type {
276 case "atomic": 281 case "atomic":
277 d := entities[x.EntityRanges[0].Key] 282 if len(x.EntityRanges) != 0 {
278 parseddescription.WriteString(`<a href="`) 283 d := entities[x.EntityRanges[0].Key]
279 parseddescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url)) 284 parsedDescription.WriteString(`<a href="`)
280 parseddescription.WriteString(`"><img width="50%" src="`) 285 parsedDescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
281 parseddescription.WriteString(ParseMedia(d.Media)) 286 parsedDescription.WriteString(`"><img width="50%" src="`)
282 parseddescription.WriteString(`" title="`) 287 parsedDescription.WriteString(ParseMedia(d.Media))
283 parseddescription.WriteString(d.Author.Username) 288 parsedDescription.WriteString(`" title="`)
284 parseddescription.WriteString(" - ") 289 parsedDescription.WriteString(d.Author.Username)
285 parseddescription.WriteString(d.Title) 290 parsedDescription.WriteString(" - ")
286 parseddescription.WriteString(`"></a>`) 291 parsedDescription.WriteString(d.Title)
292 parsedDescription.WriteString(`"></a>`)
293 }
287 case "unstyled": 294 case "unstyled":
288 if l := len(Styles); l != 0 { 295 if l := len(Styles); l != 0 {
289 for n, r := range Styles { 296 for n, r := range Styles {
@@ -292,31 +299,31 @@ func ParseDescription(dscr devianter.Text) string {
292 tag = "h2" 299 tag = "h2"
293 } 300 }
294 301
295 parseddescription.WriteString(x.Text[:r.From]) 302 parsedDescription.WriteString(x.Text[:r.From])
296 if len(urls) != 0 && len(x.EntityRanges) != 0 { 303 if len(urls) != 0 && len(x.EntityRanges) != 0 {
297 ra := &x.EntityRanges[0] 304 ra := &x.EntityRanges[0]
298 305
299 parseddescription.WriteString(`<a target="_blank" href="`) 306 parsedDescription.WriteString(`<a target="_blank" href="`)
300 parseddescription.WriteString(urls[ra.Key]) 307 parsedDescription.WriteString(urls[ra.Key])
301 parseddescription.WriteString(`">`) 308 parsedDescription.WriteString(`">`)
302 parseddescription.WriteString(r.TXT) 309 parsedDescription.WriteString(r.TXT)
303 parseddescription.WriteString(`</a>`) 310 parsedDescription.WriteString(`</a>`)
304 } else if l > n+1 { 311 } else if l > n+1 {
305 parseddescription.WriteString(r.TXT) 312 parsedDescription.WriteString(r.TXT)
306 } 313 }
307 parseddescription.WriteString(TagBuilder(tag, x.Text[r.To:])) 314 parsedDescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
308 } 315 }
309 } else { 316 } else {
310 parseddescription.WriteString(x.Text) 317 parsedDescription.WriteString(x.Text)
311 } 318 }
312 } 319 }
313 parseddescription.WriteString("<br>") 320 parsedDescription.WriteString("<br>")
314 } 321 }
315 } else if dl != 0 { 322 } else if dl != 0 {
316 for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; { 323 for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
317 switch tt.Next() { 324 switch tt.Next() {
318 case html.ErrorToken: 325 case html.ErrorToken:
319 return parseddescription.String() 326 return parsedDescription.String()
320 case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken: 327 case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
321 token := tt.Token() 328 token := tt.Token()
322 switch token.Data { 329 switch token.Data {
@@ -324,11 +331,11 @@ func ParseDescription(dscr devianter.Text) string {
324 for _, a := range token.Attr { 331 for _, a := range token.Attr {
325 if a.Key == "href" { 332 if a.Key == "href" {
326 url := DeleteTrackingFromUrl(a.Val) 333 url := DeleteTrackingFromUrl(a.Val)
327 parseddescription.WriteString(`<a target="_blank" href="`) 334 parsedDescription.WriteString(`<a target="_blank" href="`)
328 parseddescription.WriteString(url) 335 parsedDescription.WriteString(url)
329 parseddescription.WriteString(`">`) 336 parsedDescription.WriteString(`">`)
330 parseddescription.WriteString(GetValueOfTag(tt)) 337 parsedDescription.WriteString(GetValueOfTag(tt))
331 parseddescription.WriteString("</a> ") 338 parsedDescription.WriteString("</a> ")
332 } 339 }
333 } 340 }
334 case "img": 341 case "img":
@@ -344,24 +351,24 @@ func ParseDescription(dscr devianter.Text) string {
344 } 351 }
345 if title != "" { 352 if title != "" {
346 for x := -1; x < b; x++ { 353 for x := -1; x < b; x++ {
347 parseddescription.WriteString(`<img src="`) 354 parsedDescription.WriteString(`<img src="`)
348 parseddescription.WriteString(uri) 355 parsedDescription.WriteString(uri)
349 parseddescription.WriteString(`" title="`) 356 parsedDescription.WriteString(`" title="`)
350 parseddescription.WriteString(title) 357 parsedDescription.WriteString(title)
351 parseddescription.WriteString(`">`) 358 parsedDescription.WriteString(`">`)
352 } 359 }
353 } 360 }
354 } 361 }
355 case "br", "li", "ul", "p", "b": 362 case "br", "li", "ul", "p", "b":
356 parseddescription.WriteString(token.String()) 363 parsedDescription.WriteString(token.String())
357 case "div": 364 case "div":
358 parseddescription.WriteString("<p> ") 365 parsedDescription.WriteString("<p> ")
359 } 366 }
360 case html.TextToken: 367 case html.TextToken:
361 parseddescription.Write(tt.Text()) 368 parsedDescription.Write(tt.Text())
362 } 369 }
363 } 370 }
364 } 371 }
365 372
366 return parseddescription.String() 373 return parsedDescription.String()
367} 374}
app/router.go +7 −4
@@ -1,7 +1,6 @@
1package app 1package app
2 2
3import ( 3import (
4 "io"
5 "net/http" 4 "net/http"
6 u "net/url" 5 u "net/url"
7 "strconv" 6 "strconv"
@@ -44,7 +43,7 @@ func Router() {
44 43
45 // функция, что управляет всем 44 // функция, что управляет всем
46 handle := func(w http.ResponseWriter, r *http.Request) { 45 handle := func(w http.ResponseWriter, r *http.Request) {
47 if h := r.Header["Scheme"]; len(h) != 0 && h[0] == "https" { 46 if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
48 Host = h[0] + "://" + r.Host 47 Host = h[0] + "://" + r.Host
49 } else { 48 } else {
50 Host = "http://" + r.Host 49 Host = "http://" + r.Host
@@ -97,10 +96,14 @@ func Router() {
97 skunky.About() 96 skunky.About()
98 case "stylesheet": 97 case "stylesheet":
99 w.Header().Add("content-type", "text/css") 98 w.Header().Add("content-type", "text/css")
100 io.WriteString(w, Templates["skunky.css"]) 99 wr(w, Templates["skunky.css"])
100 case "favicon.ico":
101 wr(w, Templates["logo.png"])
101 } 102 }
102 } 103 }
103 104
104 http.HandleFunc("/", handle) 105 http.HandleFunc("/", handle)
105 try_with_exitstatus(http.ListenAndServe(CFG.Listen, nil), 1) 106 println("SkunkyArt is listening on", CFG.Listen)
107
108 tryWithExitStatus(http.ListenAndServe(CFG.Listen, nil), 1)
106} 109}
app/util.go +22 −32
@@ -26,7 +26,7 @@ func try(e error) {
26 println(e.Error()) 26 println(e.Error())
27 } 27 }
28} 28}
29func try_with_exitstatus(err error, code int) { 29func tryWithExitStatus(err error, code int) {
30 if err != nil { 30 if err != nil {
31 exit(err.Error(), code) 31 exit(err.Error(), code)
32 } 32 }
@@ -87,7 +87,7 @@ func Download(url string) (d Downloaded) {
87 87
88 req, e := http.NewRequest("GET", url, nil) 88 req, e := http.NewRequest("GET", url, nil)
89 try(e) 89 try(e)
90 req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0") 90 req.Header.Set("User-Agent", CFG.UserAgent)
91 91
92 resp, e := cli.Do(req) 92 resp, e := cli.Do(req)
93 try(e) 93 try(e)
@@ -148,14 +148,14 @@ func InitCacheSystem() {
148 try(e) 148 try(e)
149 for _, a := range dirnames { 149 for _, a := range dirnames {
150 a = c.Path + "/" + a 150 a = c.Path + "/" + a
151 if c.Lifetime != 0 { 151 if c.Lifetime != "" {
152 now := time.Now().UnixMilli() 152 now := time.Now().UnixMilli()
153 153
154 f, _ := os.Stat(a) 154 f, _ := os.Stat(a)
155 stat := f.Sys().(*syscall.Stat_t) 155 stat := f.Sys().(*syscall.Stat_t)
156 time := time.Unix(stat.Ctim.Unix()).UnixMilli() 156 time := time.Unix(stat.Ctim.Unix()).UnixMilli()
157 157
158 if time+c.Lifetime <= now { 158 if time+lifetimeParsed <= now {
159 try(os.RemoveAll(a)) 159 try(os.RemoveAll(a))
160 } 160 }
161 } 161 }
@@ -172,19 +172,19 @@ func InitCacheSystem() {
172func CopyTemplatesToMemory() { 172func CopyTemplatesToMemory() {
173 for _, dirname := range CFG.Dirs { 173 for _, dirname := range CFG.Dirs {
174 dir, e := os.ReadDir(dirname) 174 dir, e := os.ReadDir(dirname)
175 try_with_exitstatus(e, 1) 175 tryWithExitStatus(e, 1)
176 176
177 for _, x := range dir { 177 for _, x := range dir {
178 file, e := os.ReadFile(dirname + "/" + x.Name()) 178 file, e := os.ReadFile(dirname + "/" + x.Name())
179 try_with_exitstatus(e, 1) 179 tryWithExitStatus(e, 1)
180 Templates[x.Name()] = string(file) 180 Templates[x.Name()] = string(file)
181 } 181 }
182 } 182 }
183} 183}
184 184
185/* PARSING HELPERS */ 185/* PARSING HELPERS */
186func ParseMedia(media devianter.Media) string { 186func ParseMedia(media devianter.Media, thumb ...int) string {
187 url := devianter.UrlFromMedia(media) 187 url := devianter.UrlFromMedia(media, thumb...)
188 if len(url) != 0 && CFG.Proxy { 188 if len(url) != 0 && CFG.Proxy {
189 url = url[21:] 189 url = url[21:]
190 dot := strings.Index(url, ".") 190 dot := strings.Index(url, ".")
@@ -197,9 +197,10 @@ func ParseMedia(media devianter.Media) string {
197func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) { 197func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
198 if len(url) > 32 && url[27:32] != "stash" { 198 if len(url) > 32 && url[27:32] != "stash" {
199 url = url[27:] 199 url = url[27:]
200 toart := strings.Index(url, "/art/") 200 firstshash := strings.Index(url, "/")
201 if toart != -1 { 201 lastshash := firstshash + strings.Index(url[firstshash+1:], "/")
202 output = UrlBuilder("post", url[:toart], url[toart+5:]) 202 if lastshash != -1 {
203 output = UrlBuilder("post", url[:firstshash], url[lastshash+2:])
203 } 204 }
204 } 205 }
205 return 206 return
@@ -236,13 +237,9 @@ type DeviationList struct {
236 237
237// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации. 238// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
238func (s skunkyart) NavBase(c DeviationList) string { 239func (s skunkyart) NavBase(c DeviationList) string {
239 // TODO: сделать понятнее
240 // навигация по страницам
241 var list strings.Builder 240 var list strings.Builder
242 list.WriteString("<br>")
243 p := s.Page
244 241
245 // функция для генерации ссылок 242 list.WriteString("<br>")
246 prevrev := func(msg string, page int, onpage bool) { 243 prevrev := func(msg string, page int, onpage bool) {
247 if !onpage { 244 if !onpage {
248 list.WriteString(`<a href="?p=`) 245 list.WriteString(`<a href="?p=`)
@@ -268,33 +265,26 @@ func (s skunkyart) NavBase(c DeviationList) string {
268 } 265 }
269 } 266 }
270 267
271 // вперёд-назад 268 p := s.Page
269
272 if p > 1 { 270 if p > 1 {
273 prevrev("<= Prev |", p-1, false) 271 prevrev("<= Prev |", p-1, false)
274 } else { 272 } else {
275 p = 1 273 p = 1
276 } 274 }
277 275
278 if c.Pages > 0 { 276 for i, x := p-6, 0; (i <= c.Pages && i <= p+6) && x < 12; i++ {
279 // назад 277 if i > 0 {
280 for x := p - 6; x < p && x > 0; x++ { 278 var onPage bool
281 prevrev(strconv.Itoa(x), x, false) 279 if i == p {
282 } 280 onPage = true
283
284 // вперёд
285 for x := p; x <= p+6 && c.Pages > p+6; x++ {
286 if x == p {
287 prevrev("", x, true)
288 x++
289 } 281 }
290 282
291 if x > p { 283 prevrev(strconv.Itoa(i), i, onPage)
292 prevrev(strconv.Itoa(x), x, false) 284 x++
293 }
294 } 285 }
295 } 286 }
296 287
297 // вперёд-назад
298 if c.More { 288 if c.More {
299 prevrev("| Next =>", p+1, false) 289 prevrev("| Next =>", p+1, false)
300 } 290 }
app/wrapper.go +35 −16
@@ -48,6 +48,7 @@ type skunkyart struct {
48 } 48 }
49 49
50 SomeList string 50 SomeList string
51 DDStrips string
51 Deviation struct { 52 Deviation struct {
52 Post devianter.Post 53 Post devianter.Post
53 Related string 54 Related string
@@ -93,7 +94,10 @@ func (s skunkyart) GRUser() {
93 94
94 var g devianter.Group 95 var g devianter.Group
95 g.Name = s.Query 96 g.Name = s.Query
96 s.Templates.GroupUser.GR = g.GroupFunc() 97 var err error
98 s.Templates.GroupUser.GR, err = g.GetGroup()
99 try(err)
100
97 group := &s.Templates.GroupUser 101 group := &s.Templates.GroupUser
98 102
99 switch s.Type { 103 switch s.Type {
@@ -135,7 +139,7 @@ func (s skunkyart) GRUser() {
135 group.About.Interests += interest.String() 139 group.About.Interests += interest.String()
136 } 140 }
137 } 141 }
138 group.About.Comments = s.ParseComments(devianter.CommentsFunc( 142 group.About.Comments = s.ParseComments(devianter.GetComments(
139 strconv.Itoa(group.GR.Gruser.ID), 143 strconv.Itoa(group.GR.Gruser.ID),
140 "", 144 "",
141 s.Page, 145 s.Page,
@@ -161,9 +165,11 @@ func (s skunkyart) GRUser() {
161 s.Page++ 165 s.Page++
162 } 166 }
163 167
164 gallery := g.Gallery(s.Page, folderid) 168 gallery, err := g.GetGallery(s.Page, folderid)
169 try(err)
170
165 if folderid > 0 { 171 if folderid > 0 {
166 group.Gallery.List = s.DeviationList(gallery.Content.Results, DeviationList{ 172 group.Gallery.List = s.DeviationList(gallery.Content.Results, true, DeviationList{
167 More: gallery.Content.HasMore, 173 More: gallery.Content.HasMore,
168 }) 174 })
169 } else { 175 } else {
@@ -204,7 +210,7 @@ func (s skunkyart) GRUser() {
204 } 210 }
205 211
206 if x.Name == "folder_deviations" { 212 if x.Name == "folder_deviations" {
207 group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, DeviationList{ 213 group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, true, DeviationList{
208 Pages: x.ModuleData.Folder.Pages, 214 Pages: x.ModuleData.Folder.Pages,
209 More: x.ModuleData.Folder.HasMore, 215 More: x.ModuleData.Folder.HasMore,
210 }) 216 })
@@ -227,7 +233,7 @@ func (s skunkyart) Deviation(author, postname string) {
227 post := &s.Templates.Deviation 233 post := &s.Templates.Deviation
228 234
229 id := id_search[len(id_search)-1] 235 id := id_search[len(id_search)-1]
230 post.Post = devianter.DeviationFunc(id, author) 236 post.Post = devianter.GetDeviation(id, author)
231 237
232 if post.Post.Deviation.TextContent.Excerpt != "" { 238 if post.Post.Deviation.TextContent.Excerpt != "" {
233 post.Post.Description = ParseDescription(post.Post.Deviation.TextContent) 239 post.Post.Description = ParseDescription(post.Post.Deviation.TextContent)
@@ -239,7 +245,7 @@ func (s skunkyart) Deviation(author, postname string) {
239 post.Post.IMG = ParseMedia(post.Post.Deviation.Media) 245 post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
240 for _, x := range post.Post.Deviation.Extended.RelatedContent { 246 for _, x := range post.Post.Deviation.Extended.RelatedContent {
241 if len(x.Deviations) != 0 { 247 if len(x.Deviations) != 0 {
242 post.Related += s.DeviationList(x.Deviations) 248 post.Related += s.DeviationList(x.Deviations, false)
243 } 249 }
244 } 250 }
245 251
@@ -259,7 +265,7 @@ func (s skunkyart) Deviation(author, postname string) {
259 post.Post.Comments.Cursor = "" 265 post.Post.Comments.Cursor = ""
260 } 266 }
261 267
262 post.Comments = s.ParseComments(devianter.CommentsFunc(id, post.Post.Comments.Cursor, s.Page, 1)) 268 post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
263 269
264 s.ExecuteTemplate("deviantion.htm", &s) 270 s.ExecuteTemplate("deviantion.htm", &s)
265 } else { 271 } else {
@@ -268,25 +274,38 @@ func (s skunkyart) Deviation(author, postname string) {
268} 274}
269 275
270func (s skunkyart) DD() { 276func (s skunkyart) DD() {
271 dd := devianter.DailyDeviationsFunc(s.Page) 277 dd := devianter.GetDailyDeviations(s.Page)
272 s.Templates.SomeList = s.DeviationList(dd.Deviations, DeviationList{ 278 var strips strings.Builder
279 for _, x := range dd.Strips {
280 strips.WriteString(`<h3 class="`)
281 strips.WriteString(x.Codename)
282 strips.WriteString(`"> <a href="#`)
283 strips.WriteString(x.Codename)
284 strips.WriteString(`"># </a>`)
285 strips.WriteString(x.Title)
286 strips.WriteString(`</h3>`)
287
288 strips.WriteString(s.DeviationList(x.Deviations, false))
289 }
290 s.Templates.DDStrips = strips.String()
291 s.Templates.SomeList = s.DeviationList(dd.Deviations, true, DeviationList{
273 Pages: 0, 292 Pages: 0,
274 More: dd.HasMore, 293 More: dd.HasMore,
275 }) 294 })
276 if !s.Atom { 295 if !s.Atom {
277 s.ExecuteTemplate("list.htm", &s) 296 s.ExecuteTemplate("daily.htm", &s)
278 } 297 }
279} 298}
280 299
281func (s skunkyart) Search() { 300func (s skunkyart) Search() {
282 s.Atom = false 301 s.Atom = false
283 var e error 302 var err error
284 ss := &s.Templates.Search 303 ss := &s.Templates.Search
285 switch s.Type { 304 switch s.Type {
286 case 'a', 't': 305 case 'a', 't':
287 ss.Content, e = devianter.SearchFunc(s.Query, s.Page, s.Type) 306 ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type)
288 case 'g': 307 case 'g':
289 ss.Content, e = devianter.SearchFunc(s.Query, s.Page, s.Type, s.Args.Get("usr")) 308 ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type, s.Args.Get("usr"))
290 case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп 309 case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп
291 var ( 310 var (
292 usernames = make(map[int]string) 311 usernames = make(map[int]string)
@@ -333,10 +352,10 @@ func (s skunkyart) Search() {
333 default: 352 default:
334 s.ReturnHTTPError(400) 353 s.ReturnHTTPError(400)
335 } 354 }
336 try(e) 355 try(err)
337 356
338 if s.Type != 'r' { 357 if s.Type != 'r' {
339 ss.List = s.DeviationList(ss.Content.Results, DeviationList{ 358 ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{
340 Pages: ss.Content.Pages, 359 Pages: ss.Content.Pages,
341 More: ss.Content.HasMore, 360 More: ss.Content.HasMore,
342 }) 361 })
config.example.json +6 −4
@@ -4,15 +4,17 @@
4 "cache": { 4 "cache": {
5 "enabled": true, 5 "enabled": true,
6 "path": "/home/skunk/projects/skunkyart/cache", 6 "path": "/home/skunk/projects/skunkyart/cache",
7 "lifetime": null, 7 "lifetime": "1w",
8 "max-size": 100000, 8 "max-size": 1024,
9 "update-interval": 5 9 "update-interval": 5
10 }, 10 },
11 "dirs-to-memory": [ 11 "dirs-to-memory": [
12 "/home/skunk/projects/skunkyart/html", 12 "/home/skunk/projects/skunkyart/html",
13 "/home/skunk/projects/skunkyart/css" 13 "/home/skunk/projects/skunkyart/css",
14 "/home/skunk/projects/skunkyart/misc"
14 ], 15 ],
15 "download-proxy": null, 16 "download-proxy": "http://127.0.0.1:8080",
17 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
16 "proxy": true, 18 "proxy": true,
17 "nsfw": false 19 "nsfw": false
18} 20}
css/skunky.css +43 −7
@@ -131,28 +131,64 @@ form input, button, select {
131} 131}
132 132
133/* SCREEN OPTIMISATIONS */ 133/* SCREEN OPTIMISATIONS */
134@media screen and (orientation: portrait) { 134@media (orientation: portrait) {
135 header { 135 * {
136 scale: 155%; 136 font-size: 120%
137 justify-content: center;
138 } 137 }
138
139 ul {
140 font-size: 80%
141 }
142
143 center form {
144 font-size: 60%
145 }
146
147 header form {
148 font-size: 60%;
149 }
150 header, center {
151 text-align: center;
152 display: block;
153 clear: both;
154 font-size: 200%;
155 }
156
139 .content { 157 .content {
140 margin: auto; 158 margin: auto;
141 display: inherit; 159 display: inherit;
142 scale: 100%; 160 scale: 100%;
143 } 161 }
144 .block { 162 .block {
145 max-width: 60%; 163 margin-top: 10%;
164 max-width: 200%;
165 }
166 .folder-item {
167 width: 25%
168 }
169 .folders {
170 display: flexbox;
171 justify-content: center
172 }
173 figure img {
174 width: 10%
175 }
176 figure a img {
177 width: 100%
178 }
179 .msg {
180 font-size: 60%;
181 max-width: 80%
146 } 182 }
147} 183}
148 184
149@media screen and (max-width: 1462px) { 185@media (max-width: 1462px) and (orientation: landscape) {
150 .block { 186 .block {
151 max-width: 30%; 187 max-width: 30%;
152 } 188 }
153} 189}
154 190
155@media screen and (min-width: 788px) and (max-width: 884px) { 191@media (min-width: 788px) and (max-width: 884px) {
156 .block { 192 .block {
157 max-width: 35%; 193 max-width: 35%;
158 } 194 }
go.mod +2
@@ -2,6 +2,8 @@ module skunkyart
2 2
3go 1.22.3 3go 1.22.3
4 4
5replace git.macaw.me/skunky/devianter v0.2.0 => /home/skunk/projects/devianter
6
5require ( 7require (
6 git.macaw.me/skunky/devianter v0.2.0 8 git.macaw.me/skunky/devianter v0.2.0
7 golang.org/x/net v0.27.0 9 golang.org/x/net v0.27.0
html/about.htm +1 −1
@@ -58,6 +58,6 @@
58 </li> 58 </li>
59 {{end}} 59 {{end}}
60 </ul> 60 </ul>
61 <p>Copyright <a href="https://go.kde.org/matrix/#/@softpigeones:ebloid.ru" target="_blank">lost+skunk</a>, X11. <a href="https://git.macaw.me/skunky/skunkyart/src/tag/v1.3" target="_blank">SkunkyArt v1.3</a></p> 61 <p>Copyright <a href="https://go.kde.org/matrix/#/@softpigeones:ebloid.ru" target="_blank">lost+skunk</a>, X11. <a href="https://git.macaw.me/skunky/skunkyart/src/tag/v1.3.1" target="_blank">SkunkyArt v1.3.1</a></p>
62 </main> 62 </main>
63</html>\ No newline at end of file 63</html>\ No newline at end of file
html/list.htm → html/daily.htm renamed +6 −1
@@ -1,7 +1,7 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3 <head> 3 <head>
4 <title>SkunkyArt</title> 4 <title>SkunkyArt | Daily Deviations</title>
5 <link rel="stylesheet" href="{{.BasePath}}stylesheet"> 5 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
6 </head> 6 </head>
7 <main> 7 <main>
@@ -17,6 +17,11 @@
17 <button type="submit">Search!</button> 17 <button type="submit">Search!</button>
18 </form> 18 </form>
19 </header> 19 </header>
20 {{if ne .Templates.DDStrips ""}}
21 <h2 id="strips"><a href="#strips">#</a> Strips</h2>
22 {{.Templates.DDStrips}}
23 {{end}}
24 <h2 id="content"><a href="#content">#</a> Content</h2>
20 {{.Templates.SomeList}} 25 {{.Templates.SomeList}}
21 </main> 26 </main>
22</html>\ No newline at end of file 27</html>\ No newline at end of file
instances.json +11
@@ -44,6 +44,17 @@
44 "nsfw": true, 44 "nsfw": true,
45 "proxy": true 45 "proxy": true
46 } 46 }
47 },
48 {
49 "title": "lumaeris.com",
50 "country": "US",
51 "urls": [{
52 "clearnet": "https://skunkyart.lumaeris.com"
53 }],
54 "settings": {
55 "nsfw": true,
56 "proxy": true
57 }
47 } 58 }
48 ] 59 ]
49}\ No newline at end of file 60}\ No newline at end of file
misc/logo.png added

Binary file not shown.

services/skunkyart.example.openrc added +13
@@ -0,0 +1,13 @@
1#!/sbin/openrc-run
2
3directory=<path_to_dir_with_skunkyart>
4# command_args="-c $directory/config.json" # if SA wasn't start, try to uncomment this line
5command=$directory/skunkyart
6
7name="SkunkyArt"
8description="Privacy frontend for deviantart.com"
9supervisor=supervise-daemon
10
11depend() {
12 need net
13}\ No newline at end of file
services/skunkyart.example.service added +11
@@ -0,0 +1,11 @@
1# Note: i didn't use systemd, so it can be not works :)
2
3[Unit]
4Description=Privacy frontend for deviantart.com
5
6[Service]
7Directory=<path-to-dir-with-skunkyart>
8ExecStart=<path-to-dir-skunkyart>
9
10[Install]
11WantedBy=multi-user.target
skunkyart.example.openrc deleted −11
@@ -1,11 +0,0 @@
1#!/sbin/openrc-run
2name="SkunkyArt"
3description="Privacy frontend for deviantart.com"
4supervisor=supervise-daemon
5command=<path_to_skunkyart>
6command_args="-c <path_to_config>"
7directory="<path_to_dir_with_skunkyart>"
8
9depend() {
10 need net
11}\ No newline at end of file