mirror of
https://gitlab.com/biskuteri-cafe/JKomasto2.git
synced 2024-11-20 05:04:51 +01:00
Sort of fixed the uploadFile issue.
This commit is contained in:
parent
dc980c153c
commit
fc2880325e
0
BasicHTMLParser.java
Normal file → Executable file
0
BasicHTMLParser.java
Normal file → Executable file
0
ClipboardApi.java
Normal file → Executable file
0
ClipboardApi.java
Normal file → Executable file
0
ComposeWindow.java
Normal file → Executable file
0
ComposeWindow.java
Normal file → Executable file
0
ImageApi.java
Normal file → Executable file
0
ImageApi.java
Normal file → Executable file
0
ImageWindow.java
Normal file → Executable file
0
ImageWindow.java
Normal file → Executable file
0
JKomasto.java
Normal file → Executable file
0
JKomasto.java
Normal file → Executable file
0
KDE_Dialog_Appear.wav
Normal file → Executable file
0
KDE_Dialog_Appear.wav
Normal file → Executable file
0
LoginWindow.java
Normal file → Executable file
0
LoginWindow.java
Normal file → Executable file
92
MastodonApi.java
Normal file → Executable file
92
MastodonApi.java
Normal file → Executable file
@ -435,40 +435,86 @@ MastodonApi {
|
|||||||
assert file != null;
|
assert file != null;
|
||||||
assert file.canRead();
|
assert file.canRead();
|
||||||
|
|
||||||
String token = accessToken.get("access_token").value;
|
String bct =
|
||||||
|
"multipart/form-data; "
|
||||||
|
+ "boundary=\"JKomastoFileUpload\"";
|
||||||
|
String fsb = "--JKomastoFileUpload\r\n";
|
||||||
|
String feb = "\r\n--JKomastoFileUpload--\r\n";
|
||||||
|
String fcd =
|
||||||
|
"Content-Disposition: form-data; "
|
||||||
|
+ "name=\"file\"; "
|
||||||
|
+ "filename=\"" + file.getName() + "\"\r\n";
|
||||||
|
String fct = "Content-Type: image/png\r\n\r\n";
|
||||||
|
int contentLength = 0;
|
||||||
|
contentLength += fsb.length();
|
||||||
|
contentLength += feb.length();
|
||||||
|
contentLength += fcd.length();
|
||||||
|
contentLength += fct.length();
|
||||||
|
contentLength += file.length();
|
||||||
|
/*
|
||||||
|
* (知) This was an absurdity to debug. Contrary to
|
||||||
|
* cURL, Java sets default values for some headers,
|
||||||
|
* some of which are restricted, meaning you can't
|
||||||
|
* arbitrarily change them. Content-Length is one
|
||||||
|
* of them, set to 2^14-1 bytes. I'm pretty sure
|
||||||
|
* the file I was uploading was under this, but
|
||||||
|
* anyways one of the two parties was stopping me
|
||||||
|
* from finishing transferring my form data.
|
||||||
|
*
|
||||||
|
* They didn't mention this in the Javadocs.
|
||||||
|
* I noticed HttpURLConnection#setChunkedStreamingMode
|
||||||
|
* and #setFixedLengthStreamingMode by accident.
|
||||||
|
* Turns out, the latter is how I do what cURL and
|
||||||
|
* Firefox are doing - precalculate the exact size
|
||||||
|
* of the body and set the content length to it.
|
||||||
|
* Unfortunately, this is not flexible, we have to
|
||||||
|
* be exact. Thankfully, my answers pass..
|
||||||
|
*
|
||||||
|
* On the other side, Mastodon is obtuse as usual.
|
||||||
|
* They had code that basically throws a generic 500
|
||||||
|
* upon any sort of error from their library[1]. What
|
||||||
|
* problem the library had with my requests, I could
|
||||||
|
* never know. There is an undocumented requirement
|
||||||
|
* that you must put a filename in the content
|
||||||
|
* disposition. That one I found by guessing.
|
||||||
|
*
|
||||||
|
* I solved this with the help of -Djavax.net.debug,
|
||||||
|
* which revealed to me how my headers and body
|
||||||
|
* differed from cURL and Firefox. If this issue
|
||||||
|
* happens again, I advise giving up.
|
||||||
|
*
|
||||||
|
* [1] app/controllers/api/v1/media_controller.rb
|
||||||
|
* #create. 3 March 2022
|
||||||
|
*/
|
||||||
|
|
||||||
|
String token = accessToken.get("access_token").value;
|
||||||
String url = instanceUrl + "/api/v1/media/";
|
String url = instanceUrl + "/api/v1/media/";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
URL endpoint = new URL(url);
|
URL endpoint = new URL(url);
|
||||||
HttpURLConnection conn = cast(endpoint.openConnection());
|
HttpURLConnection conn = cast(endpoint.openConnection());
|
||||||
String s1 = "Bearer " + token;
|
String s1 = "Bearer " + token;
|
||||||
conn.setRequestProperty("Authorization", s1);
|
conn.setRequestProperty("Authorization", s1);
|
||||||
conn.setDoOutput(true);
|
conn.setDoOutput(true);
|
||||||
conn.setRequestMethod("POST");
|
conn.setRequestMethod("POST");
|
||||||
String s2 = "multipart/form-data; ";
|
conn.setFixedLengthStreamingMode(contentLength);
|
||||||
String s3 = "boundary=\"MastodonMediaUpload\"";
|
conn.setRequestProperty("Content-Type", bct);
|
||||||
conn.setRequestProperty("Content-Type", s2 + s3);
|
conn.setRequestProperty("Accept", "*/*");
|
||||||
conn.connect();
|
conn.connect();
|
||||||
|
|
||||||
OutputStream ostream = conn.getOutputStream();
|
OutputStream ostream = conn.getOutputStream();
|
||||||
Writer owriter = owriter(ostream);
|
|
||||||
InputStream istream = new FileInputStream(file);
|
InputStream istream = new FileInputStream(file);
|
||||||
// Let's see if this works!
|
|
||||||
|
ostream.write(fsb.getBytes());
|
||||||
|
ostream.write(fcd.getBytes());
|
||||||
|
ostream.write(fct.getBytes());
|
||||||
|
|
||||||
|
int c; while ((c = istream.read()) != -1)
|
||||||
|
ostream.write(c);
|
||||||
|
|
||||||
String s4, s5, s6;
|
ostream.write(feb.getBytes());
|
||||||
s4 = "--MastodonMediaUpload";
|
istream.close();
|
||||||
s5 = "Content-Disposition: form-data; name=file";
|
ostream.close();
|
||||||
s6 = "Content-Type: application/octet-stream";
|
|
||||||
owriter.write(s4 + "\r\n");
|
|
||||||
owriter.write(s5 + "\r\n");
|
|
||||||
owriter.write(s6 + "\r\n\r\n");
|
|
||||||
int c; while ((c = istream.read()) != -1)
|
|
||||||
ostream.write(c);
|
|
||||||
owriter.write("\r\n" + s4 + "--\r\n");
|
|
||||||
|
|
||||||
istream.close();
|
|
||||||
ostream.close();
|
|
||||||
|
|
||||||
wrapResponseInTree(conn, handler);
|
wrapResponseInTree(conn, handler);
|
||||||
}
|
}
|
||||||
@ -556,7 +602,7 @@ MastodonApi {
|
|||||||
int code = conn.getResponseCode();
|
int code = conn.getResponseCode();
|
||||||
if (code >= 300)
|
if (code >= 300)
|
||||||
{
|
{
|
||||||
Reader input = ireader(conn.getErrorStream());
|
Reader input = ireader(conn.getErrorStream());
|
||||||
Tree<String> response = fromPlain(input);
|
Tree<String> response = fromPlain(input);
|
||||||
input.close();
|
input.close();
|
||||||
handler.requestFailed(code, response);
|
handler.requestFailed(code, response);
|
||||||
@ -636,6 +682,7 @@ MastodonApi {
|
|||||||
ireader(InputStream is)
|
ireader(InputStream is)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
|
assert is != null;
|
||||||
return new InputStreamReader(is);
|
return new InputStreamReader(is);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,6 +690,7 @@ MastodonApi {
|
|||||||
owriter(OutputStream os)
|
owriter(OutputStream os)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
|
assert os != null;
|
||||||
return new OutputStreamWriter(os);
|
return new OutputStreamWriter(os);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0
NotificationsWindow.java
Normal file → Executable file
0
NotificationsWindow.java
Normal file → Executable file
35
PostWindow.java
Normal file → Executable file
35
PostWindow.java
Normal file → Executable file
@ -350,7 +350,7 @@ PostWindow extends JFrame {
|
|||||||
this.primaire = primaire;
|
this.primaire = primaire;
|
||||||
this.api = primaire.getMastodonApi();
|
this.api = primaire.getMastodonApi();
|
||||||
|
|
||||||
getContentPane().setPreferredSize(new Dimension(360, 270));
|
getContentPane().setPreferredSize(new Dimension(360, 260));
|
||||||
pack();
|
pack();
|
||||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||||
setLocationByPlatform(true);
|
setLocationByPlatform(true);
|
||||||
@ -411,6 +411,9 @@ implements ActionListener {
|
|||||||
deletePost,
|
deletePost,
|
||||||
redraftPost;
|
redraftPost;
|
||||||
|
|
||||||
|
private Image
|
||||||
|
backgroundImage;
|
||||||
|
|
||||||
// ---%-@-%---
|
// ---%-@-%---
|
||||||
|
|
||||||
public void
|
public void
|
||||||
@ -485,7 +488,7 @@ implements ActionListener {
|
|||||||
public void
|
public void
|
||||||
resetFocus()
|
resetFocus()
|
||||||
{
|
{
|
||||||
bodyScrollPane.getVerticalScrollBar().setValue(0);
|
//bodyScrollPane.getVerticalScrollBar().setValue(0);
|
||||||
media.requestFocusInWindow();
|
media.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -531,14 +534,16 @@ implements ActionListener {
|
|||||||
|
|
||||||
if (src == nextPrev)
|
if (src == nextPrev)
|
||||||
{
|
{
|
||||||
if (command.equals("next"))
|
if (command.startsWith("next"))
|
||||||
{
|
{
|
||||||
|
body.nextPage();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
body.previousPage();
|
||||||
}
|
}
|
||||||
|
// First time an interactive element
|
||||||
|
// doesn't call something in primaire..
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,6 +572,18 @@ implements ActionListener {
|
|||||||
lay1 = RichTextPane.layout(authorNameOr, fm1, w1);
|
lay1 = RichTextPane.layout(authorNameOr, fm1, w1);
|
||||||
authorName.setText(lay1);
|
authorName.setText(lay1);
|
||||||
|
|
||||||
|
if (backgroundImage != null)
|
||||||
|
{
|
||||||
|
int tw = backgroundImage.getWidth(this);
|
||||||
|
int th = backgroundImage.getHeight(this);
|
||||||
|
if (tw != -1)
|
||||||
|
for (int y = 0; y < getHeight(); y += th)
|
||||||
|
for (int x = 0; x < getWidth(); x += tw)
|
||||||
|
{
|
||||||
|
g.drawImage(backgroundImage, x, y, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
((java.awt.Graphics2D)g).setRenderingHint(
|
((java.awt.Graphics2D)g).setRenderingHint(
|
||||||
java.awt.RenderingHints.KEY_ANTIALIASING,
|
java.awt.RenderingHints.KEY_ANTIALIASING,
|
||||||
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
||||||
@ -644,10 +661,12 @@ implements ActionListener {
|
|||||||
time.setFont(f1);
|
time.setFont(f1);
|
||||||
|
|
||||||
JPanel top1 = new JPanel();
|
JPanel top1 = new JPanel();
|
||||||
|
top1.setOpaque(false);
|
||||||
top1.setLayout(new BorderLayout(8, 0));
|
top1.setLayout(new BorderLayout(8, 0));
|
||||||
top1.add(authorId);
|
top1.add(authorId);
|
||||||
top1.add(date, BorderLayout.EAST);
|
top1.add(date, BorderLayout.EAST);
|
||||||
JPanel top2 = new JPanel();
|
JPanel top2 = new JPanel();
|
||||||
|
top2.setOpaque(false);
|
||||||
top2.setLayout(new BorderLayout(8, 0));
|
top2.setLayout(new BorderLayout(8, 0));
|
||||||
top2.add(authorName);
|
top2.add(authorName);
|
||||||
top2.add(time, BorderLayout.EAST);
|
top2.add(time, BorderLayout.EAST);
|
||||||
@ -659,6 +678,7 @@ implements ActionListener {
|
|||||||
body = new RichTextPane3();
|
body = new RichTextPane3();
|
||||||
body.setFont(f3);
|
body.setFont(f3);
|
||||||
|
|
||||||
|
/*
|
||||||
bodyScrollPane = new JScrollPane(
|
bodyScrollPane = new JScrollPane(
|
||||||
body,
|
body,
|
||||||
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
|
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
|
||||||
@ -669,18 +689,21 @@ implements ActionListener {
|
|||||||
vsb.setUnitIncrement(16);
|
vsb.setUnitIncrement(16);
|
||||||
bodyScrollPane.setBorder(null);
|
bodyScrollPane.setBorder(null);
|
||||||
bodyScrollPane.setFocusable(true);
|
bodyScrollPane.setFocusable(true);
|
||||||
|
*/
|
||||||
|
|
||||||
JPanel centre = new JPanel();
|
JPanel centre = new JPanel();
|
||||||
centre.setOpaque(false);
|
centre.setOpaque(false);
|
||||||
centre.setLayout(new BorderLayout(0, 8));
|
centre.setLayout(new BorderLayout(0, 8));
|
||||||
centre.add(top, BorderLayout.NORTH);
|
centre.add(top, BorderLayout.NORTH);
|
||||||
centre.add(bodyScrollPane);
|
centre.add(body);
|
||||||
|
|
||||||
setLayout(new BorderLayout(8, 0));
|
setLayout(new BorderLayout(8, 0));
|
||||||
add(left, BorderLayout.WEST);
|
add(left, BorderLayout.WEST);
|
||||||
add(centre);
|
add(centre);
|
||||||
|
|
||||||
setBorder(b);
|
setBorder(b);
|
||||||
|
|
||||||
|
backgroundImage = ImageApi.local("postWindow");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
0
ProfileWindow.java
Normal file → Executable file
0
ProfileWindow.java
Normal file → Executable file
0
RepliesWindow.java
Normal file → Executable file
0
RepliesWindow.java
Normal file → Executable file
0
RequestListener.java
Normal file → Executable file
0
RequestListener.java
Normal file → Executable file
5
RichTextPane.java
Normal file → Executable file
5
RichTextPane.java
Normal file → Executable file
@ -69,7 +69,9 @@ implements MouseListener, MouseMotionListener, KeyListener {
|
|||||||
{
|
{
|
||||||
g.setFont(getFont());
|
g.setFont(getFont());
|
||||||
FontMetrics fm = g.getFontMetrics(getFont());
|
FontMetrics fm = g.getFontMetrics(getFont());
|
||||||
g.clearRect(0, 0, getWidth(), getHeight());
|
|
||||||
|
if (isOpaque())
|
||||||
|
g.clearRect(0, 0, getWidth(), getHeight());
|
||||||
|
|
||||||
((java.awt.Graphics2D)g).setRenderingHint(
|
((java.awt.Graphics2D)g).setRenderingHint(
|
||||||
java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
|
java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
|
||||||
@ -403,6 +405,7 @@ implements MouseListener, MouseMotionListener, KeyListener {
|
|||||||
RichTextPane()
|
RichTextPane()
|
||||||
{
|
{
|
||||||
text = new LinkedList<>();
|
text = new LinkedList<>();
|
||||||
|
|
||||||
addMouseListener(this);
|
addMouseListener(this);
|
||||||
addMouseMotionListener(this);
|
addMouseMotionListener(this);
|
||||||
addKeyListener(this);
|
addKeyListener(this);
|
||||||
|
0
RichTextPane2.java
Normal file → Executable file
0
RichTextPane2.java
Normal file → Executable file
388
RichTextPane3.java
Normal file → Executable file
388
RichTextPane3.java
Normal file → Executable file
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
import java.awt.Graphics;
|
import java.awt.Graphics;
|
||||||
import java.awt.Point;
|
|
||||||
import java.awt.FontMetrics;
|
import java.awt.FontMetrics;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
import java.awt.Image;
|
import java.awt.Image;
|
||||||
@ -33,12 +32,15 @@ implements
|
|||||||
private Map<String, Image>
|
private Map<String, Image>
|
||||||
emojis;
|
emojis;
|
||||||
|
|
||||||
private Map<Tree<String>, Point>
|
private Map<Tree<String>, Position>
|
||||||
layout;
|
layout;
|
||||||
|
|
||||||
private Tree<String>
|
private Tree<String>
|
||||||
layoutEnd, selStart, selEnd;
|
layoutEnd, selStart, selEnd;
|
||||||
|
|
||||||
|
private int
|
||||||
|
startingLine, endingLine;
|
||||||
|
|
||||||
// ---%-@-%---
|
// ---%-@-%---
|
||||||
|
|
||||||
public void
|
public void
|
||||||
@ -55,31 +57,34 @@ implements
|
|||||||
assert html.get("children") != null;
|
assert html.get("children") != null;
|
||||||
|
|
||||||
FontMetrics fm = getFontMetrics(getFont());
|
FontMetrics fm = getFontMetrics(getFont());
|
||||||
int iy = fm.getAscent();
|
Position cursor = new Position(0, 1);
|
||||||
Point cursor = new Point(0, iy);
|
|
||||||
|
|
||||||
Tree<String> nodes = html.get("children");
|
// Manually negate if first element is a break.
|
||||||
if (nodes.size() > 0)
|
Tree<String> children = html.get("children");
|
||||||
|
if (children.size() > 0)
|
||||||
{
|
{
|
||||||
Tree<String> first = nodes.get(0);
|
Tree<String> first = children.get(0);
|
||||||
if (first.key.equals("tag"))
|
if (first.key.equals("tag"))
|
||||||
{
|
{
|
||||||
String tagName = first.get(0).key;
|
String tagName = first.get(0).key;
|
||||||
if (tagName.equals("p"))
|
if (tagName.equals("br")) cursor.line -= 1;
|
||||||
{
|
if (tagName.equals("p")) cursor.line -= 2;
|
||||||
int lh = fm.getAscent() + fm.getDescent();
|
}
|
||||||
cursor.y -= lh * 2;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selStart = selEnd = null;
|
selStart = selEnd = null;
|
||||||
layout.clear();
|
layout.clear();
|
||||||
|
startingLine = 1;
|
||||||
layout(html, fm, cursor);
|
layout(html, fm, cursor);
|
||||||
layout.put(layoutEnd, new Point(cursor));
|
layout.put(layoutEnd, cursor.clone());
|
||||||
|
endingLine = cursor.line;
|
||||||
repaint();
|
repaint();
|
||||||
|
|
||||||
setPreferredSize(new Dimension(1, cursor.y));
|
int iy = fm.getAscent();
|
||||||
|
int lh = fm.getAscent() + fm.getDescent();
|
||||||
|
int h = snap2(cursor.line, iy, lh);
|
||||||
|
h += fm.getDescent();
|
||||||
|
setPreferredSize(new Dimension(1, h));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
@ -90,26 +95,43 @@ implements
|
|||||||
setText(html);
|
setText(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void
|
||||||
|
previousPage()
|
||||||
|
{
|
||||||
|
int advance = getHeightInLines();
|
||||||
|
if (startingLine < advance) startingLine = 1;
|
||||||
|
else startingLine -= advance;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void
|
||||||
|
nextPage()
|
||||||
|
{
|
||||||
|
int advance = getHeightInLines();
|
||||||
|
if (endingLine - startingLine < advance) return;
|
||||||
|
else startingLine += advance;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
// - -%- -
|
// - -%- -
|
||||||
|
|
||||||
private void
|
private void
|
||||||
layout(Tree<String> node, FontMetrics fm, Point cursor)
|
layout(Tree<String> node, FontMetrics fm, Position cursor)
|
||||||
{
|
{
|
||||||
assert cursor != null;
|
assert cursor != null;
|
||||||
int lh = fm.getAscent() + fm.getDescent();
|
|
||||||
|
|
||||||
if (node.key.equals("space"))
|
if (node.key.equals("space"))
|
||||||
{
|
{
|
||||||
int w = fm.stringWidth(node.value);
|
int w = fm.stringWidth(node.value);
|
||||||
if (cursor.x + w < getWidth())
|
if (cursor.x + w < getWidth())
|
||||||
{
|
{
|
||||||
layout.put(node, new Point(cursor));
|
layout.put(node, cursor.clone());
|
||||||
cursor.x += w;
|
cursor.x += w;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
layout.put(node, new Point(cursor));
|
layout.put(node, cursor.clone());
|
||||||
cursor.y += lh;
|
++cursor.line;
|
||||||
cursor.x = 0;
|
cursor.x = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,14 +140,14 @@ implements
|
|||||||
int w = fm.stringWidth(node.value);
|
int w = fm.stringWidth(node.value);
|
||||||
if (cursor.x + w < getWidth())
|
if (cursor.x + w < getWidth())
|
||||||
{
|
{
|
||||||
layout.put(node, new Point(cursor));
|
layout.put(node, cursor.clone());
|
||||||
cursor.x += w;
|
cursor.x += w;
|
||||||
}
|
}
|
||||||
else if (w < getWidth())
|
else if (w < getWidth())
|
||||||
{
|
{
|
||||||
cursor.y += lh;
|
++cursor.line;
|
||||||
cursor.x = 0;
|
cursor.x = 0;
|
||||||
layout.put(node, new Point(cursor));
|
layout.put(node, cursor.clone());
|
||||||
cursor.x += w;
|
cursor.x += w;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -138,7 +160,7 @@ implements
|
|||||||
w = fm.charWidth(node.value.charAt(0));
|
w = fm.charWidth(node.value.charAt(0));
|
||||||
if (w >= aw)
|
if (w >= aw)
|
||||||
{
|
{
|
||||||
cursor.y += lh;
|
++cursor.line;
|
||||||
cursor.x = 0;
|
cursor.x = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,18 +179,12 @@ implements
|
|||||||
Tree<String> temp = new Tree<>();
|
Tree<String> temp = new Tree<>();
|
||||||
temp.key = node.key;
|
temp.key = node.key;
|
||||||
temp.value = substr;
|
temp.value = substr;
|
||||||
layout.put(temp, new Point(cursor));
|
layout.put(temp, cursor.clone());
|
||||||
|
|
||||||
rem.delete(0, l);
|
rem.delete(0, l);
|
||||||
if (rem.length() == 0)
|
boolean more = rem.length() != 0;
|
||||||
{
|
if (more) ++cursor.line;
|
||||||
cursor.x = w;
|
cursor.x = more ? 0 : w;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cursor.y += lh;
|
|
||||||
cursor.x = 0;
|
|
||||||
}
|
|
||||||
aw = mw;
|
aw = mw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,28 +192,28 @@ implements
|
|||||||
else if (node.key.equals("emoji"))
|
else if (node.key.equals("emoji"))
|
||||||
{
|
{
|
||||||
Image image = emojis.get(node.value);
|
Image image = emojis.get(node.value);
|
||||||
int w;
|
int w; if (image != null)
|
||||||
if (image != null)
|
|
||||||
{
|
{
|
||||||
int ow = image.getWidth(this);
|
int ow = image.getWidth(this);
|
||||||
int oh = image.getHeight(this);
|
int oh = image.getHeight(this);
|
||||||
int h = lh;
|
int h = fm.getAscent() + fm.getDescent();
|
||||||
w = ow * h/oh;
|
w = ow * h/oh;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
w = fm.stringWidth(node.value);
|
w = fm.stringWidth(node.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor.x + w < getWidth())
|
if (cursor.x + w < getWidth())
|
||||||
{
|
{
|
||||||
layout.put(node, new Point(cursor));
|
layout.put(node, cursor.clone());
|
||||||
cursor.x += w;
|
cursor.x += w;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
cursor.y += lh;
|
++cursor.line;
|
||||||
cursor.x = 0;
|
cursor.x = 0;
|
||||||
layout.put(node, new Point(cursor));
|
layout.put(node, cursor.clone());
|
||||||
cursor.x += w;
|
cursor.x += w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,31 +226,32 @@ implements
|
|||||||
|
|
||||||
if (tagName.equals("br"))
|
if (tagName.equals("br"))
|
||||||
{
|
{
|
||||||
cursor.y += lh;
|
++cursor.line;
|
||||||
cursor.x = 0;
|
cursor.x = 0;
|
||||||
}
|
}
|
||||||
else if (tagName.equals("p"))
|
else if (tagName.equals("p"))
|
||||||
{
|
{
|
||||||
//cursor.y += lh * 3/2;
|
//cursor.line += 3/2;
|
||||||
cursor.y += lh * 2;
|
cursor.line += 2;
|
||||||
// Our selection algorithm assumes equidistant
|
// We don't have vertical cursor movement
|
||||||
// lines. Laziest fix is collect and sort line
|
// other than the line. Maybe fix in the
|
||||||
// Ys from height.
|
// future..?
|
||||||
cursor.x = 0;
|
cursor.x = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Tree<String> child: children)
|
for (Tree<String> child: children)
|
||||||
{
|
{
|
||||||
// Shallow copy this child node.
|
// Shallow copy this child node,
|
||||||
Tree<String> aug = new Tree<>();
|
Tree<String> aug = new Tree<>();
|
||||||
aug.key = child.key;
|
aug.key = child.key;
|
||||||
aug.value = child.value;
|
aug.value = child.value;
|
||||||
for (Tree<String> gc: child) aug.add(gc);
|
for (Tree<String> gc: child) aug.add(gc);
|
||||||
|
|
||||||
// Append all of our attributes. We'd like those
|
// Append all of our attributes. We'd like
|
||||||
// like href to end up at the text nodes. This
|
// those like href to end up at the text
|
||||||
// might collide with our child node's attributes,
|
// nodes. This might collide with our
|
||||||
// for now I'll assume that's not an issue.
|
// child node's attributes, for now I'll
|
||||||
|
// assume that's not an issue.
|
||||||
for (int o = 1; o < node.size(); ++o)
|
for (int o = 1; o < node.size(); ++o)
|
||||||
{
|
{
|
||||||
Tree<String> attr = node.get(o);
|
Tree<String> attr = node.get(o);
|
||||||
@ -257,80 +274,88 @@ implements
|
|||||||
|
|
||||||
g.setFont(getFont());
|
g.setFont(getFont());
|
||||||
FontMetrics fm = g.getFontMetrics();
|
FontMetrics fm = g.getFontMetrics();
|
||||||
|
int iy = fm.getAscent();
|
||||||
|
int lh = fm.getAscent() + fm.getDescent();
|
||||||
|
int asc = fm.getAscent();
|
||||||
|
int w = getWidth(), h = getHeight();
|
||||||
|
|
||||||
((java.awt.Graphics2D)g).setRenderingHint(
|
if (isOpaque()) g.clearRect(0, 0, w, h);
|
||||||
java.awt.RenderingHints.KEY_ANTIALIASING,
|
|
||||||
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
|
||||||
);
|
|
||||||
|
|
||||||
if (selEnd != null)
|
if (selEnd != null)
|
||||||
{
|
{
|
||||||
Point ssp = layout.get(selStart);
|
Position ssp = layout.get(selStart);
|
||||||
assert ssp != null;
|
assert ssp != null;
|
||||||
Point sep = layout.get(selEnd);
|
Position sep = layout.get(selEnd);
|
||||||
assert sep != null;
|
assert sep != null;
|
||||||
/*
|
/*
|
||||||
* (知) One way these can go null is if we clear
|
* (知) One way these can go null is if we clear
|
||||||
* the layout but don't clear the selection.
|
* the layout but don't clear the selection.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
boolean flip = ssp.y > sep.y;
|
if (ssp.compareTo(sep) > 0)
|
||||||
flip |= sep.y == ssp.y && sep.x < ssp.x;
|
|
||||||
if (flip)
|
|
||||||
{
|
{
|
||||||
Point temp = ssp;
|
Position temp = ssp;
|
||||||
ssp = sep;
|
ssp = sep;
|
||||||
sep = ssp;
|
sep = ssp;
|
||||||
}
|
}
|
||||||
|
|
||||||
int w = getWidth();
|
int ls = 1 + ssp.line - startingLine;
|
||||||
int asc = fm.getAscent();
|
int le = 1 + sep.line - startingLine;
|
||||||
int lh = fm.getAscent() + fm.getDescent();
|
int ys = snap2(ls, iy, lh) - asc;
|
||||||
g.setColor(SEL_COLOUR);
|
int ye = snap2(le, iy, lh) - asc;
|
||||||
if (ssp.y == sep.y)
|
|
||||||
|
g.setColor(SEL_COLOUR);
|
||||||
|
if (ssp.line == sep.line)
|
||||||
{
|
{
|
||||||
g.fillRect(ssp.x, ssp.y - asc, sep.x - ssp.x, lh);
|
g.fillRect(ssp.x, ys, sep.x - ssp.x, lh);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
g.fillRect(ssp.x, ssp.y - asc, w - ssp.x, lh);
|
g.fillRect(ssp.x, ys, w - ssp.x, lh);
|
||||||
for (int y = ssp.y + lh; y < sep.y; y += lh)
|
for (int l = ls + 1; l < le; ++l)
|
||||||
{
|
{
|
||||||
g.fillRect(0, y - asc, w, lh);
|
int y = snap2(l, iy, lh) - asc;
|
||||||
|
g.fillRect(0, y, w, lh);
|
||||||
}
|
}
|
||||||
g.fillRect(0, sep.y - asc, sep.x, lh);
|
g.fillRect(0, ye, sep.x, lh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
((java.awt.Graphics2D)g).setRenderingHint(
|
||||||
|
java.awt.RenderingHints.KEY_ANTIALIASING,
|
||||||
|
java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
||||||
|
);
|
||||||
|
|
||||||
g.setColor(getForeground());
|
g.setColor(getForeground());
|
||||||
for (Tree<String> node: layout.keySet())
|
for (Tree<String> node: layout.keySet())
|
||||||
{
|
{
|
||||||
if (node.key.equals("text"))
|
Position position = layout.get(node);
|
||||||
|
int x = position.x;
|
||||||
|
int line = 1 + position.line - startingLine;
|
||||||
|
int y = snap2(line, iy, lh);
|
||||||
|
if (y > h) continue;
|
||||||
|
|
||||||
|
if (node.key.equals("text"))
|
||||||
{
|
{
|
||||||
boolean isLink = node.get("href") != null;
|
boolean isLink = node.get("href") != null;
|
||||||
if (isLink) g.setColor(LINK_COLOUR);
|
if (isLink) g.setColor(LINK_COLOUR);
|
||||||
Point point = layout.get(node);
|
g.drawString(node.value, x, y);
|
||||||
g.drawString(node.value, point.x, point.y);
|
|
||||||
if (isLink) g.setColor(PLAIN_COLOUR);
|
if (isLink) g.setColor(PLAIN_COLOUR);
|
||||||
}
|
}
|
||||||
else if (node.key.equals("emoji"))
|
else if (node.key.equals("emoji"))
|
||||||
{
|
{
|
||||||
Point point = layout.get(node);
|
Image image = emojis.get(node.value);
|
||||||
Image image = emojis.get(node.value);
|
|
||||||
if (image != null)
|
if (image != null)
|
||||||
{
|
{
|
||||||
int ow = image.getWidth(this);
|
int ow = image.getWidth(this);
|
||||||
int oh = image.getHeight(this);
|
int oh = image.getHeight(this);
|
||||||
int nh = fm.getAscent() + fm.getDescent();
|
int nh = fm.getAscent() + fm.getDescent();
|
||||||
int nw = ow * nh/oh;
|
int nw = ow * nh/oh;
|
||||||
int x = point.x;
|
y -= asc;
|
||||||
int y = point.y - fm.getAscent();
|
|
||||||
g.drawImage(image, x, y, nw, nh, this);
|
g.drawImage(image, x, y, nw, nh, this);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int x = point.x;
|
|
||||||
int y = point.y;
|
|
||||||
g.drawString(node.value, x, y);
|
g.drawString(node.value, x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -350,36 +375,74 @@ implements
|
|||||||
mouseDragged(MouseEvent eM)
|
mouseDragged(MouseEvent eM)
|
||||||
{
|
{
|
||||||
if (selStart == null) return;
|
if (selStart == null) return;
|
||||||
selEnd = identifyNodeAt(eM.getX(), eM.getY());
|
selEnd = identifyNodeAfter(eM.getX(), eM.getY());
|
||||||
if (selEnd == null) selEnd = layoutEnd;
|
if (selEnd == null) selEnd = layoutEnd;
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void
|
||||||
|
mouseMoved(MouseEvent eM)
|
||||||
|
{
|
||||||
|
Tree<String> h = identifyNodeAt(eM.getX(), eM.getY());
|
||||||
|
if (h == null || h.get("href") == null)
|
||||||
|
{
|
||||||
|
setToolTipText("");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setToolTipText(h.get("href").value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Tree<String>
|
private Tree<String>
|
||||||
identifyNodeAt(int x, int y)
|
identifyNodeAt(int x, int y)
|
||||||
{
|
{
|
||||||
FontMetrics fm = getFontMetrics(getFont());
|
FontMetrics fm = getFontMetrics(getFont());
|
||||||
int initial = fm.getAscent();
|
int initial = fm.getAscent();
|
||||||
int advance = fm.getAscent() + fm.getDescent();
|
int advance = fm.getAscent() + fm.getDescent();
|
||||||
y = snap(y, initial, advance);
|
int line = isnap2(y, initial, advance);
|
||||||
|
|
||||||
Tree<String> returnee = null;
|
Tree<String> returnee = null;
|
||||||
|
Position closest = new Position(0, 0);
|
||||||
int maxX = 0;
|
for (Tree<String> node: layout.keySet())
|
||||||
for (Tree<String> node: layout.keySet())
|
|
||||||
{
|
{
|
||||||
Point point = layout.get(node);
|
Position position = layout.get(node);
|
||||||
assert point != null;
|
assert position != null;
|
||||||
|
|
||||||
if (point.y != y) continue;
|
if (position.line != line) continue;
|
||||||
if (point.x > x) continue;
|
if (position.x > x) continue;
|
||||||
if (point.x >= maxX)
|
if (position.x >= closest.x)
|
||||||
{
|
{
|
||||||
maxX = point.x;
|
returnee = node;
|
||||||
returnee = node;
|
closest = position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return returnee;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tree<String>
|
||||||
|
identifyNodeAfter(int x, int y)
|
||||||
|
{
|
||||||
|
FontMetrics fm = getFontMetrics(getFont());
|
||||||
|
int initial = fm.getAscent();
|
||||||
|
int advance = fm.getAscent() + fm.getDescent();
|
||||||
|
int line = isnap2(y, initial, advance);
|
||||||
|
|
||||||
|
Tree<String> returnee = null;
|
||||||
|
Position closest = new Position(Integer.MAX_VALUE, 0);
|
||||||
|
for (Tree<String> node: layout.keySet())
|
||||||
|
{
|
||||||
|
Position position = layout.get(node);
|
||||||
|
assert position != null;
|
||||||
|
|
||||||
|
if (position.line != line) continue;
|
||||||
|
if (position.x < x) continue;
|
||||||
|
if (position.x < closest.x)
|
||||||
|
{
|
||||||
|
returnee = node;
|
||||||
|
closest = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
return returnee;
|
return returnee;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,52 +468,47 @@ implements
|
|||||||
{
|
{
|
||||||
assert selStart != null && selEnd != null;
|
assert selStart != null && selEnd != null;
|
||||||
|
|
||||||
Point ssp = layout.get(selStart);
|
Position ssp = layout.get(selStart);
|
||||||
Point sep = layout.get(selEnd);
|
Position sep = layout.get(selEnd);
|
||||||
assert ssp != null && sep != null;
|
assert ssp != null && sep != null;
|
||||||
boolean flip = ssp.y > sep.y;
|
if (ssp.compareTo(sep) > 1)
|
||||||
flip |= sep.y == ssp.y && sep.x < ssp.x;
|
|
||||||
if (flip)
|
|
||||||
{
|
{
|
||||||
Point temp = ssp;
|
Position temp = ssp;
|
||||||
ssp = sep;
|
ssp = sep;
|
||||||
sep = ssp;
|
sep = ssp;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Tree<String>> selected = new ArrayList<>();
|
List<Tree<String>> selected = new ArrayList<>();
|
||||||
List<Point> points = new ArrayList<>();
|
List<Position> positions = new ArrayList<>();
|
||||||
for (Tree<String> node: layout.keySet())
|
for (Tree<String> node: layout.keySet())
|
||||||
{
|
{
|
||||||
Point point = layout.get(node);
|
Position position = layout.get(node);
|
||||||
assert point != null;
|
assert position != null;
|
||||||
|
|
||||||
boolean c1 = point.y == ssp.y && point.x >= ssp.x;
|
boolean after = position.compareTo(ssp) > 0;
|
||||||
boolean c2 = point.y == sep.y && point.x < sep.x;
|
boolean before = position.compareTo(sep) < 0;
|
||||||
boolean c3 = point.y > ssp.y && point.y < sep.y;
|
if (!(after || before)) continue;
|
||||||
if (!(c1 || c2 || c3)) continue;
|
|
||||||
|
|
||||||
// Just throw them in a pile for now..
|
// Just throw them in a pile for now..
|
||||||
selected.add(node);
|
selected.add(node);
|
||||||
points.add(point);
|
positions.add(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now sort them into reading order.
|
// Now sort them into reading order.
|
||||||
Tree<String> n1, n2;
|
Tree<String> n1, n2;
|
||||||
Point p1, p2;
|
Position p1, p2;
|
||||||
for (int eo = 1; eo < points.size(); ++eo)
|
for (int eo = 1; eo < positions.size(); ++eo)
|
||||||
for (int o = points.size() - 1; o >= eo; --o)
|
for (int o = positions.size() - 1; o >= eo; --o)
|
||||||
{
|
{
|
||||||
n1 = selected.get(o - 1); n2 = selected.get(o);
|
n1 = selected.get(o - 1); n2 = selected.get(o);
|
||||||
p1 = points.get(o - 1); p2 = points.get(o);
|
p1 = positions.get(o - 1); p2 = positions.get(o);
|
||||||
|
|
||||||
boolean c1 = p2.y < p1.y;
|
if (p2.compareTo(p1) > 0) continue;
|
||||||
boolean c2 = p2.y == p1.y && p2.x < p1.x;
|
|
||||||
if (!(c1 || c2)) continue;
|
|
||||||
|
|
||||||
selected.set(o - 1, n2);
|
selected.set(o - 1, n2);
|
||||||
selected.set(o, n1);
|
selected.set(o, n1);
|
||||||
points.set(o - 1, p2);
|
positions.set(o - 1, p2);
|
||||||
points.set(o, p1);
|
positions.set(o, p1);
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder b = new StringBuilder();
|
StringBuilder b = new StringBuilder();
|
||||||
@ -460,11 +518,37 @@ implements
|
|||||||
boolean e = node.key.equals("emoji");
|
boolean e = node.key.equals("emoji");
|
||||||
boolean s = node.key.equals("space");
|
boolean s = node.key.equals("space");
|
||||||
assert t || e || s;
|
assert t || e || s;
|
||||||
b.append(node.value); // Same behaviour for all.
|
b.append(node.value);
|
||||||
|
/*
|
||||||
|
* I actually want to copy the link if the node is
|
||||||
|
* associated with one. However, a link has
|
||||||
|
* multiple text nodes, so I'd end up copying
|
||||||
|
* multiple times. The correct action is to
|
||||||
|
* associate the nodes with the same link object,
|
||||||
|
* then mark that as copied. Or, associate the
|
||||||
|
* nodes with their superiors in the HTML, then
|
||||||
|
* walk up until we find an anchor with a href.
|
||||||
|
* Then again, have to mark that as copied too.
|
||||||
|
*
|
||||||
|
* I can also walk the HTML and copy any that are
|
||||||
|
* in the selected region, careful to copy an
|
||||||
|
* anchor's href in stead of the anchor contents.
|
||||||
|
* I'd need a guarantee that my walking order is
|
||||||
|
* the same as how they were rendered on the screen.
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
return b.toString();
|
return b.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int
|
||||||
|
getHeightInLines()
|
||||||
|
{
|
||||||
|
FontMetrics fm = getFontMetrics(getFont());
|
||||||
|
int initial = fm.getAscent();
|
||||||
|
int advance = fm.getAscent() + fm.getDescent();
|
||||||
|
return isnap2(getHeight(), initial, advance);
|
||||||
|
}
|
||||||
|
|
||||||
public void
|
public void
|
||||||
keyReleased(KeyEvent eK) { }
|
keyReleased(KeyEvent eK) { }
|
||||||
|
|
||||||
@ -477,9 +561,6 @@ implements
|
|||||||
public void
|
public void
|
||||||
mouseClicked(MouseEvent eM) { }
|
mouseClicked(MouseEvent eM) { }
|
||||||
|
|
||||||
public void
|
|
||||||
mouseMoved(MouseEvent eM) { }
|
|
||||||
|
|
||||||
public void
|
public void
|
||||||
mouseEntered(MouseEvent eM) { }
|
mouseEntered(MouseEvent eM) { }
|
||||||
|
|
||||||
@ -501,15 +582,58 @@ implements
|
|||||||
// - -%- -
|
// - -%- -
|
||||||
|
|
||||||
private static int
|
private static int
|
||||||
snap(int value, int initial, int advance)
|
snap2(int value, int initial, int advance)
|
||||||
{
|
{
|
||||||
int offset = value - initial;
|
return initial + (value - 1) * advance;
|
||||||
if (offset <= 0) offset = 0;
|
// If you'd like to go behind the first line 1,
|
||||||
else {
|
// note that the first negative line is 0.
|
||||||
int lines = 1 + ((offset - 1) / advance);
|
}
|
||||||
offset = advance * lines;
|
|
||||||
}
|
private static int
|
||||||
return initial + offset;
|
isnap2(int value, int initial, int advance)
|
||||||
|
{
|
||||||
|
int offset = value - initial;
|
||||||
|
return 1 + ((offset - 1) / advance);
|
||||||
|
// Mostly correct for negative numbers. I just
|
||||||
|
// need this function to accept negative numbers,
|
||||||
|
// not give usable results.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---%-@-%---
|
||||||
|
|
||||||
|
private static class
|
||||||
|
Position {
|
||||||
|
|
||||||
|
int
|
||||||
|
x, line;
|
||||||
|
|
||||||
|
// -=%=-
|
||||||
|
|
||||||
|
public int
|
||||||
|
compareTo(Position other)
|
||||||
|
{
|
||||||
|
if (line < other.line) return -1;
|
||||||
|
if (line > other.line) return -1;
|
||||||
|
if (x < other.x) return -1;
|
||||||
|
if (x > other.x) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -=%=-
|
||||||
|
|
||||||
|
public
|
||||||
|
Position(int x, int line)
|
||||||
|
{
|
||||||
|
this.x = x;
|
||||||
|
this.line = line;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Position
|
||||||
|
clone()
|
||||||
|
{
|
||||||
|
return new Position(x, line);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---%-@-%---
|
// ---%-@-%---
|
||||||
|
0
RudimentaryHTMLParser.java
Normal file → Executable file
0
RudimentaryHTMLParser.java
Normal file → Executable file
0
TimelineWindow.java
Normal file → Executable file
0
TimelineWindow.java
Normal file → Executable file
0
TwoToggleButton.java
Normal file → Executable file
0
TwoToggleButton.java
Normal file → Executable file
0
WindowUpdater.java
Normal file → Executable file
0
WindowUpdater.java
Normal file → Executable file
BIN
graphics/nextToggled.png
Normal file
BIN
graphics/nextToggled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
BIN
graphics/nextUntoggled.png
Normal file
BIN
graphics/nextUntoggled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
BIN
graphics/postWindow.png
Normal file
BIN
graphics/postWindow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 978 B |
BIN
graphics/prevToggled.png
Normal file
BIN
graphics/prevToggled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
graphics/prevUntoggled.png
Normal file
BIN
graphics/prevUntoggled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
0
notifOptions.txt
Normal file → Executable file
0
notifOptions.txt
Normal file → Executable file
0
notifOptions.txt~
Normal file → Executable file
0
notifOptions.txt~
Normal file → Executable file
Loading…
Reference in New Issue
Block a user