sora 24.02.25 → 24.8.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sora/version.rb CHANGED
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
- module Sora
4
- VERSION = "24.02.25"
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Sora
4
+ VERSION = "24.8.4"
5
+ end
data/lib/sora.rb CHANGED
@@ -15,13 +15,15 @@ module Sora
15
15
  DocumentRoot: document_root,
16
16
  BindAddress: "0.0.0.0",
17
17
  Port: port,
18
- CGIInterpreter: RbConfig.ruby
18
+ CGIInterpreter: RbConfig.ruby,
19
+ DoNotReverseLookup: true
19
20
  }
20
21
  )
21
22
  srv.mount("/cgi-bin", WEBrick::HTTPServlet::CGIHandler, "#{document_root}/../cgi-bin/main.rb")
23
+ srv.mount("/get", WEBrick::HTTPServlet::CGIHandler, "#{document_root}/../cgi-bin/get.rb")
22
24
  srv.mount("/", WEBrick::HTTPServlet::FileHandler, "#{document_root}/index.html")
23
25
  srv.mount("/style", WEBrick::HTTPServlet::FileHandler, "#{document_root}/style.css")
24
- srv.mount("/js", WEBrick::HTTPServlet::FileHandler, "#{document_root}/main.js")
26
+ srv.mount("/js", WEBrick::HTTPServlet::FileHandler, "#{document_root}/bundle.js")
25
27
  trap("INT") { srv.shutdown }
26
28
  srv.start
27
29
  end
data/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "sora",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "repository": "git@github.com:Himeyama/sora.git",
6
+ "author": "MURATA Mitsuharu <hikari.photon+dev@gmail.com>",
7
+ "license": "MIT",
8
+ "devDependencies": {
9
+ "@fluentui/web-components": "^2.6.1",
10
+ "css-loader": "^7.1.2",
11
+ "i18next": "^23.12.2",
12
+ "style-loader": "^4.0.0",
13
+ "ts-loader": "^9.5.1",
14
+ "typescript": "^5.5.4",
15
+ "webpack": "^5.93.0",
16
+ "webpack-cli": "^5.1.4"
17
+ }
18
+ }
data/sig/sora.rbs CHANGED
@@ -1,4 +1,4 @@
1
- module Sora
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end
1
+ module Sora
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data/src/index.ts ADDED
@@ -0,0 +1,364 @@
1
+ require('./style.css')
2
+ import { fluentProgress, fluentListbox, fluentOption, fluentButton, fluentCard, fluentTextField, fluentSwitch, provideFluentDesignSystem, Listbox, TextArea, Button } from '@fluentui/web-components'
3
+ import i18next from 'i18next'
4
+
5
+ provideFluentDesignSystem()
6
+ .register(
7
+ fluentProgress(),
8
+ fluentListbox(),
9
+ fluentOption(),
10
+ fluentButton(),
11
+ fluentCard(),
12
+ fluentTextField(),
13
+ fluentSwitch()
14
+ )
15
+
16
+ const setAttribute = (id: string) => {
17
+ const element = document.getElementById(id)
18
+ if(element != null) element.setAttribute("placeholder", i18next.t(id))
19
+ }
20
+
21
+ const setText = (id: string) => {
22
+ const element = document.getElementById(id)
23
+ if(element != null) element.textContent = i18next.t(id)
24
+ }
25
+
26
+ i18next.init({
27
+ lng: 'ja', // if you're using a language detector, do not define the lng option
28
+ debug: true,
29
+ resources: {
30
+ en: {
31
+ translation: {
32
+ "title-sora": "sora",
33
+ "input-text": "Input text...",
34
+ "select-file": "Select a file...",
35
+ "send-data": "Send",
36
+ "remove-file": "Remove a file",
37
+ "remove-text": "Remove a text",
38
+ "copy-text": "Copy a text",
39
+ "open-file": "Open a file",
40
+ "download-file": "Download a file"
41
+ }
42
+ },
43
+ ja: {
44
+ translation: {
45
+ "title-sora": "ソラ",
46
+ "input-text": "テキストを入力...",
47
+ "select-file": "ファイルの選択",
48
+ "send-data": "送信",
49
+ "remove-file": "ファイルの削除",
50
+ "remove-text": "テキストの削除",
51
+ "copy-text": "テキストのコピー",
52
+ "open-file": "ファイルを開く",
53
+ "download-file": "ファイルのダウンロード"
54
+ }
55
+ }
56
+ }
57
+ }, () => {
58
+ setText("title-sora")
59
+ setAttribute("input-text")
60
+ setText("select-file")
61
+ setText("send-data")
62
+ setText("remove-file")
63
+ setText("remove-text")
64
+ setText("copy-text")
65
+ setText("open-file")
66
+ setText("download-file")
67
+ })
68
+
69
+ const progress = (isOn: boolean) => {
70
+ const progressBar = document.getElementById("progress")
71
+ if(isOn)
72
+ progressBar.classList.remove("hidden-progress")
73
+ else
74
+ progressBar.classList.add("hidden-progress")
75
+ }
76
+
77
+ // 与えられたオブジェクトのキーと値をエンコードして、HTMLフォームのクエリ文字列として連結
78
+ const encodeHTMLForm = (data: Record<string, string>) => {
79
+ const params: string[] = []
80
+ for (const name in data) {
81
+ const value = data[name]
82
+ const param = encodeURIComponent(name) + '=' + encodeURIComponent(value)
83
+ params.push(param)
84
+ }
85
+ return params.join('&').replace(/%20/g, '+')
86
+ }
87
+
88
+ const getFiles = () => {
89
+ const xhr = new XMLHttpRequest()
90
+ xhr.open("POST", "/cgi-bin")
91
+ xhr.send(encodeHTMLForm({ "files": "" }))
92
+ xhr.onreadystatechange = () => {
93
+ let texts = JSON.parse(xhr.responseText || "null")
94
+ if (texts == null)
95
+ return
96
+
97
+ // const fileListUl = document.getElementById('file-list-ul')
98
+ // fileListUl.innerHTML = ""
99
+
100
+ // const fileList = document.getElementById('file-list')
101
+ // fileList.style.display = texts.length == 0 ? "none" : "block"
102
+ // fileListWrap.style.display = texts.length == 0 ? "none" : "block"
103
+
104
+ for(const fileName of texts){
105
+ const div = document.createElement('li')
106
+ div.textContent = fileName
107
+
108
+ div.addEventListener('contextmenu', (e) => {
109
+ e.preventDefault()
110
+ contextmenuFile.style.left = e.pageX + 'px'
111
+ contextmenuFile.style.top = e.pageY + 'px'
112
+ contextmenuFile.style.display = 'block'
113
+ fileTarget = e.target
114
+ })
115
+ div.addEventListener('click', () => {
116
+ contextmenuFile.style.display = 'none'
117
+ location.href = `/get?filename=${fileName}`
118
+ })
119
+
120
+ // fileListUl.append(div)
121
+ }
122
+ }
123
+ }
124
+
125
+ // ファイルアップロード
126
+ const selectFileButtonPre = document.getElementById("select-file-button-pre")
127
+ const selectFile = document.getElementById("select-file")
128
+ if(selectFile != null && selectFileButtonPre != null){
129
+ selectFile.addEventListener("click", () => selectFileButtonPre.click())
130
+ selectFileButtonPre.addEventListener("change", () => {
131
+ const form: HTMLFormElement = document.getElementById("upload-form") as HTMLFormElement
132
+ const formData: FormData = new FormData(form)
133
+
134
+ const xhr = new XMLHttpRequest()
135
+ xhr.open("POST", "/cgi-bin")
136
+ xhr.upload.addEventListener('loadstart', () => {
137
+ console.log("Upload: start")
138
+ })
139
+
140
+ xhr.upload.addEventListener('load', () => {
141
+ // アップロード正常終了
142
+ console.log('Upload: done')
143
+ getFiles()
144
+ refresh()
145
+ })
146
+
147
+ progress(true)
148
+ xhr.send(formData)
149
+ })
150
+ }
151
+
152
+ // 子要素を削除
153
+ const removeChild = (element: HTMLElement) => {
154
+ const children: HTMLCollection = element.children
155
+ for(const child of Array.from(children)){
156
+ child.remove()
157
+ }
158
+ }
159
+
160
+ // リストを更新
161
+ const showList = (data: any, files: string[]) => {
162
+ const contents = document.getElementById("contents")
163
+ removeChild(contents)
164
+
165
+ // テキスト一覧を追加
166
+ for (const content of data) {
167
+ const text = content.content
168
+ const uuid = content.uuid
169
+
170
+ const fluentOpt = document.createElement("fluent-option")
171
+ fluentOpt.textContent = text
172
+ fluentOpt.setAttribute("value", uuid)
173
+ fluentOpt.setAttribute("type", "text")
174
+
175
+ fluentOpt.addEventListener("click", () => {
176
+ const items = document.getElementById("items")
177
+ items.classList.add("visible")
178
+ items.classList.remove("type-file")
179
+ items.classList.add("type-text")
180
+
181
+ const textData = document.getElementById("text-title")
182
+ textData.textContent = fluentOpt.textContent
183
+
184
+ const textContent = document.getElementById("text-content")
185
+ textContent.textContent = fluentOpt.textContent
186
+ })
187
+
188
+ contents.appendChild(fluentOpt)
189
+ }
190
+
191
+ // ファイル一覧を追加
192
+ for (const text of files){
193
+ const fluentOpt = document.createElement("fluent-option")
194
+ fluentOpt.textContent = text
195
+ fluentOpt.setAttribute("type", "file")
196
+
197
+ fluentOpt.addEventListener("click", () => {
198
+ const items = document.getElementById("items")
199
+ items.classList.add("visible")
200
+ items.classList.add("type-file")
201
+ items.classList.remove("type-text")
202
+
203
+ const textData = document.getElementById("file-title")
204
+ textData.textContent = fluentOpt.textContent
205
+ })
206
+ contents.appendChild(fluentOpt)
207
+ }
208
+ }
209
+
210
+ const refresh = (visibleDetails: boolean = false) => {
211
+ // 一覧を表示
212
+ fetch('/cgi-bin?data=')
213
+ .then((response) => response.json())
214
+ .then((data) => {
215
+ // showList(data)
216
+ fetch('/cgi-bin?files')
217
+ .then((response) => response.json())
218
+ .then((files) => {
219
+ showList(data, files)
220
+ progress(false)
221
+ if(visibleDetails) {
222
+ const items = document.getElementById("items")
223
+ items.classList.remove("visible")
224
+ }
225
+ }
226
+ )
227
+ }
228
+ )
229
+ }
230
+
231
+ refresh()
232
+
233
+ // データ送信
234
+ document.getElementById("send-data").addEventListener("click", () => {
235
+ progress(true)
236
+ const text: string = (document.getElementById("input-text") as HTMLInputElement).value
237
+ fetch('/cgi-bin?data=' + encodeURI(text))
238
+ .then((response) => response.json())
239
+ .then((data) => {
240
+ // showList(data)
241
+ fetch('/cgi-bin?files')
242
+ .then((response) => response.json())
243
+ .then((files) => {
244
+ showList(data, files)
245
+ progress(false)
246
+ }
247
+ )
248
+ }
249
+ )
250
+ })
251
+
252
+ /* テキストのコピーがクリックされたとき */
253
+ const copyText = document.getElementById("copy-text") as Button
254
+ copyText.addEventListener("click", () => {
255
+ const contents = document.getElementById("contents") as Listbox
256
+ if(contents.selectedIndex != -1){
257
+ // console.log(contents)
258
+ const selected = contents.selectedOptions[0]
259
+ selected.textContent
260
+
261
+ const textarea: HTMLTextAreaElement = document.createElement("textarea")
262
+ textarea.value = selected.textContent
263
+ document.body.appendChild(textarea)
264
+ textarea.select()
265
+ document.execCommand("copy")
266
+ document.body.removeChild(textarea)
267
+ }
268
+ })
269
+
270
+ /* テキストの削除がクリックされたとき */
271
+ const removeText = document.getElementById("remove-text") as Button
272
+ removeText.addEventListener("click", () => {
273
+ progress(true)
274
+ const contents = document.getElementById("contents") as Listbox
275
+ let uuid: string
276
+ if(contents.selectedIndex != -1){
277
+ const selected = contents.selectedOptions[0]
278
+ uuid = selected.value
279
+ }else{
280
+ return
281
+ }
282
+
283
+ fetch('/cgi-bin?delete=' + uuid)
284
+ .then((response) => response.json())
285
+ .then((_data) => {
286
+ refresh(true)
287
+ }
288
+ )
289
+ })
290
+
291
+ /* ファイルの削除がクリックされたとき */
292
+ const removeFile = document.getElementById("remove-file") as Button
293
+ removeFile.addEventListener("click", () => {
294
+ progress(true)
295
+ const contents = document.getElementById("contents") as Listbox
296
+ let fileName: string
297
+ if(contents.selectedIndex != -1){
298
+ const selected = contents.selectedOptions[0]
299
+ fileName = selected.textContent
300
+ }else{
301
+ return
302
+ }
303
+
304
+ fetch('/cgi-bin?removefile=' + fileName)
305
+ .then((response) => response.json())
306
+ .then((_data) => {
307
+ refresh(true)
308
+ }
309
+ )
310
+ refresh()
311
+ })
312
+
313
+ const downloadFile = (url: string, filename: string): void => {
314
+ "use strict"
315
+
316
+ // XMLHttpRequestオブジェクトを作成する
317
+ const xhr: XMLHttpRequest = new XMLHttpRequest()
318
+ xhr.open("GET", url, true)
319
+ xhr.responseType = "blob" // Blobオブジェクトとしてダウンロードする
320
+ xhr.onload = (): void => {
321
+ // ダウンロード完了後の処理を定義する
322
+ const blob: Blob = xhr.response
323
+ // Blobオブジェクトを指すURLオブジェクトを作る
324
+ const objectURL: string = window.URL.createObjectURL(blob)
325
+ // リンク(<a>要素)を生成し、JavaScriptからクリックする
326
+ const link: HTMLAnchorElement = document.createElement("a")
327
+ document.body.appendChild(link)
328
+ link.href = objectURL
329
+ link.download = filename
330
+ link.click()
331
+ document.body.removeChild(link)
332
+ }
333
+ // XMLHttpRequestオブジェクトの通信を開始する
334
+ xhr.send()
335
+ }
336
+
337
+ /* ファイルを開く */
338
+ const openFile = document.getElementById("open-file") as Button
339
+ openFile.addEventListener("click", () => {
340
+ const contents = document.getElementById("contents") as Listbox
341
+ let fileName: string
342
+ if(contents.selectedIndex != -1){
343
+ const selected = contents.selectedOptions[0]
344
+ fileName = selected.textContent
345
+ window.open(`/get?filename=${fileName}`, '_blank')
346
+ }else{
347
+ return
348
+ }
349
+ })
350
+
351
+ /* ファイルをダウンロード */
352
+ const downloadFileE = document.getElementById("download-file") as Button
353
+ downloadFileE.addEventListener("click", () => {
354
+ const contents = document.getElementById("contents") as Listbox
355
+ let fileName: string
356
+ if(contents.selectedIndex != -1){
357
+ const selected = contents.selectedOptions[0]
358
+ fileName = selected.textContent
359
+ downloadFile(`/get?filename=${fileName}`, fileName)
360
+ }else{
361
+ return
362
+ }
363
+ })
364
+
data/src/style.css ADDED
@@ -0,0 +1,157 @@
1
+ body {
2
+ /* background-color: green; */
3
+ height: 100vh;
4
+ margin: 0;
5
+ }
6
+
7
+ main {
8
+ padding: 8px;
9
+ display: grid;
10
+ grid-template-rows: auto 1fr auto;
11
+ height: calc(100% - 19px);
12
+ gap: 8px;
13
+ }
14
+
15
+ #items {
16
+ grid-row: 2 / 3;
17
+ display: grid;
18
+ grid-template-columns: 1fr auto;
19
+ transition: 1s;
20
+ }
21
+
22
+ #items.visible {
23
+ gap: 8px;
24
+ }
25
+
26
+ #items #contents {
27
+ grid-column: 1 / 2;
28
+ overflow-x: auto;
29
+ height: calc(100vh - 66px);
30
+ }
31
+
32
+ #items #details {
33
+ padding: 0;
34
+ width: 0;
35
+ border: unset;
36
+ box-shadow: unset;
37
+ }
38
+
39
+ #items.visible #details {
40
+ padding: 8px;
41
+ width: 480px;
42
+ border: solid 1px #F3F3F3;
43
+ }
44
+
45
+ #items.type-text #details .file-details {
46
+ display: none;
47
+ }
48
+
49
+ #items.type-file #details .text-details {
50
+ display: none;
51
+ }
52
+
53
+ #items.type-file #details .file-details {
54
+ display: grid;
55
+ grid-template-rows: auto 1fr auto auto;
56
+ gap: 8px;
57
+ height: 100%;
58
+ }
59
+
60
+ #items.type-file #details .file-details #file-title {
61
+ grid-row: 1 / 2;
62
+ margin: 8px 4px 0 4px;
63
+ }
64
+
65
+ #items.type-file #details .file-details #operation-file {
66
+ grid-row: 3 / 4;
67
+ display: grid;
68
+ grid-template-columns: 1fr 1fr;
69
+ gap: 8px;
70
+ }
71
+
72
+ #items.type-file #details .file-details #remove-file {
73
+ grid-row: 4 / 5;
74
+ }
75
+
76
+ /* テキスト */
77
+ #items.type-text #details .text-details {
78
+ display: grid;
79
+ grid-template-rows: auto auto auto auto;
80
+ gap: 8px;
81
+ }
82
+
83
+ #items.type-text #details .text-details #text-title {
84
+ grid-row: 1 / 2;
85
+ white-space: nowrap;
86
+ text-overflow: ellipsis;
87
+ overflow-x: hidden;
88
+ margin: 8px 4px 0 4px;
89
+ user-select: none;
90
+ height: 36px;
91
+ }
92
+
93
+ #items.type-text #details .text-details #text-content {
94
+ grid-row: 2 / 3;
95
+ margin: 0 4px;
96
+ height: calc(100vh - 217px);
97
+ overflow-y: auto;
98
+ }
99
+
100
+ #items.type-text #details .text-details #remove-text {
101
+ grid-row: 4 / 5;
102
+ }
103
+ /* ここまで */
104
+
105
+
106
+ #input-content{
107
+ grid-row: 3 / 4;
108
+ display: grid;
109
+ grid-template-columns: 1fr auto auto;
110
+ gap: 8px;
111
+ }
112
+
113
+ #contents fluent-option {
114
+ flex-shrink: 0;
115
+ margin: 2px 4px;
116
+ }
117
+
118
+ /* 進捗バー */
119
+ #progress.hidden-progress {
120
+ visibility: hidden;
121
+ }
122
+
123
+ @media only screen and (max-width: 768px) {
124
+ main {
125
+ display: block;
126
+ }
127
+
128
+ #items {
129
+ grid-template-columns: unset;
130
+ /* grid-template-rows: auto auto; */
131
+ display: block;
132
+ }
133
+
134
+ #items #contents {
135
+ height: unset;
136
+ margin-top: 8px;
137
+ width: calc(100vw - 16px);
138
+ }
139
+
140
+ #items #details {
141
+ height: 0;
142
+ }
143
+
144
+ #items.visible #details {
145
+ width: unset;
146
+ height: auto;
147
+ margin-top: 8px;
148
+ }
149
+
150
+ #items.type-text #details .text-details #text-content {
151
+ height: unset;
152
+ }
153
+
154
+ #input-content {
155
+ margin-top: 8px;
156
+ }
157
+ }
data/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "./dist/",
4
+ "noImplicitAny": true,
5
+ "module": "es6",
6
+ "target": "es5",
7
+ "jsx": "react",
8
+ "allowJs": true,
9
+ "moduleResolution": "node"
10
+ }
11
+ }
data/webpack.config.js ADDED
@@ -0,0 +1,25 @@
1
+ const path = require('path');
2
+
3
+ module.exports = {
4
+ entry: './src/index.ts',
5
+ module: {
6
+ rules: [
7
+ {
8
+ test: /\.tsx?$/,
9
+ use: 'ts-loader',
10
+ exclude: /node_modules/,
11
+ },
12
+ {
13
+ test: /\.css$/i,
14
+ use: ["style-loader", "css-loader"],
15
+ },
16
+ ],
17
+ },
18
+ resolve: {
19
+ extensions: ['.tsx', '.ts', '.js'],
20
+ },
21
+ output: {
22
+ filename: 'bundle.js',
23
+ path: path.resolve(__dirname, 'dist'),
24
+ },
25
+ }