Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63f7ab7bf6 | ||
|
|
676ff316d7 | ||
|
|
3eccb0bd4a | ||
|
|
351c61f613 | ||
|
|
333e762539 | ||
|
|
280a222210 | ||
|
|
4853eeb0a8 | ||
|
|
9e9bf01b5b | ||
|
|
8989442fc1 | ||
|
|
befe5b5977 | ||
|
|
11585f284c | ||
|
|
9503943b71 | ||
|
|
d38cf80818 | ||
|
|
3e03f15825 | ||
|
|
99e0538596 | ||
|
|
2a209519e1 | ||
|
|
461dc6f384 | ||
|
|
409aa51f1c | ||
|
|
3620fcb5b0 | ||
|
|
ca7c22b0d7 | ||
|
|
318aeb4187 | ||
|
|
1fb3bf63da | ||
|
|
aa5c4861db | ||
|
|
a7c5afdd83 | ||
|
|
38fc655a53 | ||
|
|
4c34d33ac0 | ||
|
|
eb9f84f650 | ||
|
|
443e2db167 | ||
|
|
347c570cde | ||
|
|
f8e2fa6128 | ||
|
|
9dbb979764 | ||
|
|
062db5d13a | ||
|
|
788c7c9440 | ||
|
|
aed602c55e | ||
|
|
24f4648274 | ||
|
|
d29d601513 | ||
|
|
f21d38b080 | ||
|
|
a0c64508c9 | ||
|
|
11084f04e9 | ||
|
|
2a8aa5f3c1 | ||
|
|
c64b8026ae | ||
|
|
ed76d85fd8 | ||
|
|
b2b3723ba8 | ||
|
|
ca9ce54061 | ||
|
|
8199c451b1 | ||
|
|
66173d5a38 | ||
|
|
a5fba341d3 | ||
|
|
689d3bd39b | ||
|
|
0b077ae973 | ||
|
|
9a6bb033bf | ||
|
|
7b84bab217 | ||
|
|
1a56a8996e | ||
|
|
37bcc5e026 | ||
|
|
6a158f5176 | ||
|
|
0b7ca6cb14 | ||
|
|
303f78c3bc | ||
|
|
c0c87e2c10 | ||
|
|
babce57c80 | ||
|
|
ca5866ac13 | ||
|
|
508e255c8b | ||
|
|
aeeea4fd95 | ||
|
|
6029ee539e | ||
|
|
c9a596111c | ||
|
|
b8a73cc003 | ||
|
|
817eb4d9e8 | ||
|
|
20e3e736c2 | ||
|
|
20352c0301 | ||
|
|
e7b7000f46 | ||
|
|
fb3bec623a | ||
|
|
976c066004 | ||
|
|
304a9744a9 | ||
|
|
ee11cae8dc | ||
|
|
04314f116d | ||
|
|
b645244378 | ||
|
|
110ff56aa1 | ||
|
|
bbbeb9524f | ||
|
|
90fc7532ba | ||
|
|
fac622ef97 | ||
|
|
e29b94a576 | ||
|
|
911b3691b3 | ||
|
|
3a84376223 | ||
|
|
0f5a8e44f1 | ||
|
|
5753fb2714 | ||
|
|
4008660a35 | ||
|
|
1cb58e1e0f | ||
|
|
08e9170a80 | ||
|
|
4198a5bac6 | ||
|
|
295ae13705 | ||
|
|
0894f0e777 | ||
|
|
2966ecc651 | ||
|
|
49c168b5b2 | ||
|
|
8e0017e928 | ||
|
|
982add8fbb | ||
|
|
8065e19c85 | ||
|
|
9e59439226 | ||
|
|
5087c299d3 | ||
|
|
bddf5874d4 | ||
|
|
6c469e5d0d | ||
|
|
8c88ece3dc | ||
|
|
eb28a44cc8 | ||
|
|
240e9e93d9 | ||
|
|
eebec73fd2 | ||
|
|
1b226791b4 | ||
|
|
ad9885ce92 | ||
|
|
16418ab205 | ||
|
|
d9d82a1679 | ||
|
|
ff52516324 | ||
|
|
abd3c24f68 | ||
|
|
04ef618295 | ||
|
|
a58c93be8a | ||
|
|
5a8d6b34c1 | ||
|
|
9233e4d373 | ||
|
|
6e50af16a7 | ||
|
|
046479071b | ||
|
|
ffbed73669 | ||
|
|
03371cf645 | ||
|
|
eb606b5f6c | ||
|
|
df927516b1 | ||
|
|
fa23e095e5 | ||
|
|
2298176c3b | ||
|
|
3f7caf4ad4 | ||
|
|
db9363e7af | ||
|
|
ce36761c64 | ||
|
|
4e38cfdb5c | ||
|
|
1c7c88a9ca | ||
|
|
c7d208de23 | ||
|
|
a7639344a1 | ||
|
|
ff76531962 | ||
|
|
dc77f16a34 | ||
|
|
bcd03e7e60 | ||
|
|
92bea5d715 | ||
|
|
37cd4dff6f | ||
|
|
d62bf10eaf | ||
|
|
3afbe5674b | ||
|
|
6d5a699db6 | ||
|
|
e19885a594 | ||
|
|
6e27eb751c | ||
|
|
8e3586f315 | ||
|
|
e994ab6214 | ||
|
|
4906f9dc27 | ||
|
|
55c6da07d9 | ||
|
|
8dc83a5d5a | ||
|
|
94d5732f6a | ||
|
|
f4a9f84061 | ||
|
|
f147481606 | ||
|
|
ebb0b63411 | ||
|
|
8248816577 | ||
|
|
db454b0ebc | ||
|
|
74664612fd | ||
|
|
f60b8795dc | ||
|
|
a3ad273b5f | ||
|
|
50f1638805 | ||
|
|
cf8762560e | ||
|
|
f8262dbe7f | ||
|
|
37b44da41f | ||
|
|
9693fefa81 | ||
|
|
9ed9d732f6 | ||
|
|
ef1b37995f | ||
|
|
8e1e1c0eca | ||
|
|
4c0a1fd5ad | ||
|
|
70c26e9fb1 | ||
|
|
40ed13cbba | ||
|
|
2ee64724d0 | ||
|
|
88550940a3 | ||
|
|
20aea17a55 | ||
|
|
4393e3271e | ||
|
|
0487dbdcde | ||
|
|
58e38a0a10 | ||
|
|
e83b367e3f | ||
|
|
b82b486314 | ||
|
|
d292cd00b7 | ||
|
|
86db47c291 | ||
|
|
181807e090 | ||
|
|
fe24f1ea0d | ||
|
|
f4e96e32fd | ||
|
|
f1c4ce06fb | ||
|
|
af483e155b | ||
|
|
b4a2c261b5 | ||
|
|
4df54be0df |
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: '[<SOURCE>]'
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Please add the source type for your bug to the title of the issue, e.g. "[rss] RSS Feed could not be parsed". When
|
||||
your problem is not related to a source use "[core]", e.g. "[core] Feature xyz is not working"
|
||||
-->
|
||||
|
||||
## Describe the Bug
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## Platform
|
||||
|
||||
- [ ] Android
|
||||
- [ ] iOS
|
||||
- [ ] Linux
|
||||
- [ ] macOS
|
||||
- [ ] Windows
|
||||
- [ ] Web
|
||||
|
||||
## To Reproduce
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
## Expected behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Screenshots
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context about the problem here.
|
||||
31
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: '[<SOURCE>]'
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Please add the source type for your bug to the title of the issue, e.g. "[rss] RSS Feed could not be parsed". When
|
||||
your problem is not related to a source use "core", e.g. "[core] Feature xyz is not working"
|
||||
-->
|
||||
|
||||
## Is your feature request related to a problem? Please describe
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...]
|
||||
|
||||
## Describe the solution you'd like
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
## Describe alternatives you've considered
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've
|
||||
considered.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
Keep PR title verbose enough and add prefix telling about what source it touches e.g "[rss] Add feature xyz" or if the
|
||||
the PR is not realated to a source use "[core]", e.g. "[core] Fix xyz".
|
||||
|
||||
If you add a breaking change within your PR you should add ":warning:" to the title,
|
||||
e.g. ":warning: [core] My breaking change"
|
||||
-->
|
||||
|
||||
<!--
|
||||
Description of what have been changed. Please also reference an issue, when available.
|
||||
-->
|
||||
BIN
.github/assets/badge-app-store.png
vendored
|
Before Width: | Height: | Size: 20 KiB |
47
.github/assets/badge-app-store.svg
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 120 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M110.135,0L9.535,0C9.168,0 8.806,0 8.44,0.002C8.134,0.004 7.83,0.01 7.521,0.015C6.85,0.023 6.18,0.082 5.517,0.191C4.856,0.304 4.215,0.515 3.616,0.818C3.019,1.124 2.472,1.522 1.998,1.997C1.52,2.471 1.122,3.018 0.819,3.618C0.515,4.217 0.305,4.859 0.194,5.521C0.083,6.183 0.023,6.853 0.015,7.523C0.006,7.83 0.005,8.138 0,8.444L0,31.559C0.005,31.869 0.006,32.17 0.015,32.481C0.023,33.151 0.083,33.821 0.194,34.482C0.304,35.145 0.515,35.788 0.819,36.387C1.122,36.985 1.52,37.53 1.998,38.001C2.471,38.478 3.017,38.876 3.616,39.18C4.215,39.484 4.855,39.697 5.517,39.81C6.18,39.919 6.85,39.978 7.521,39.987C7.83,39.994 8.134,39.998 8.44,39.998C8.806,40 9.168,40 9.535,40L110.135,40C110.494,40 110.859,40 111.219,39.998C111.523,39.998 111.836,39.994 112.141,39.987C112.811,39.979 113.479,39.92 114.141,39.81C114.804,39.696 115.448,39.483 116.049,39.18C116.647,38.876 117.194,38.478 117.666,38.001C118.142,37.528 118.541,36.983 118.848,36.387C119.15,35.787 119.358,35.145 119.467,34.482C119.578,33.821 119.64,33.151 119.652,32.481C119.656,32.17 119.656,31.869 119.656,31.559C119.664,31.195 119.664,30.834 119.664,30.465L119.664,9.536C119.664,9.17 119.664,8.807 119.656,8.444C119.656,8.138 119.656,7.83 119.652,7.523C119.64,6.852 119.578,6.183 119.467,5.521C119.358,4.859 119.149,4.217 118.848,3.618C118.23,2.415 117.251,1.436 116.049,0.818C115.448,0.516 114.804,0.304 114.141,0.191C113.48,0.081 112.811,0.022 112.141,0.015C111.836,0.01 111.523,0.004 111.219,0.002C110.859,-0 110.494,-0 110.135,-0L110.135,0Z" style="fill:rgb(166,166,166);fill-rule:nonzero;"/>
|
||||
<path d="M8.445,39.125C8.14,39.125 7.843,39.121 7.541,39.114C6.914,39.106 6.29,39.052 5.671,38.951C5.095,38.852 4.537,38.667 4.015,38.403C3.498,38.142 3.026,37.798 2.618,37.387C2.204,36.98 1.859,36.508 1.597,35.99C1.333,35.469 1.149,34.91 1.054,34.333C0.951,33.713 0.896,33.086 0.888,32.458C0.881,32.247 0.873,31.545 0.873,31.545L0.873,8.444C0.873,8.444 0.882,7.753 0.888,7.55C0.895,6.922 0.951,6.297 1.053,5.678C1.149,5.099 1.332,4.539 1.597,4.016C1.857,3.498 2.2,3.026 2.612,2.618C3.023,2.206 3.496,1.861 4.014,1.595C4.535,1.332 5.092,1.149 5.667,1.051C6.288,0.95 6.915,0.895 7.543,0.887L8.445,0.875L111.214,0.875L112.127,0.888C112.75,0.895 113.371,0.949 113.985,1.05C114.566,1.149 115.13,1.334 115.656,1.598C116.694,2.133 117.539,2.979 118.071,4.018C118.332,4.538 118.512,5.094 118.606,5.667C118.71,6.291 118.768,6.922 118.78,7.554C118.783,7.837 118.783,8.142 118.783,8.444C118.791,8.819 118.791,9.176 118.791,9.536L118.791,30.465C118.791,30.828 118.791,31.183 118.783,31.54C118.783,31.865 118.783,32.163 118.779,32.47C118.768,33.091 118.71,33.71 118.608,34.323C118.515,34.904 118.333,35.468 118.068,35.993C117.805,36.506 117.462,36.973 117.053,37.379C116.644,37.793 116.172,38.138 115.653,38.401C115.128,38.667 114.566,38.853 113.985,38.951C113.367,39.052 112.742,39.107 112.116,39.114C111.823,39.121 111.517,39.125 111.219,39.125L110.135,39.127L8.445,39.125Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g id="_Group_">
|
||||
<g id="_Group_2_" serif:id="_Group_2">
|
||||
<g id="_Group_3_" serif:id="_Group_3">
|
||||
<path id="_Path_" d="M24.769,20.301C24.791,18.606 25.682,17.036 27.125,16.149C26.21,14.842 24.729,14.041 23.134,13.991C21.455,13.815 19.827,14.996 18.971,14.996C18.099,14.996 16.782,14.008 15.363,14.038C13.498,14.098 11.797,15.135 10.89,16.766C8.956,20.114 10.399,25.035 12.251,27.742C13.178,29.067 14.261,30.547 15.679,30.495C17.066,30.437 17.584,29.61 19.258,29.61C20.917,29.61 21.403,30.495 22.849,30.462C24.338,30.437 25.275,29.13 26.17,27.792C26.836,26.848 27.348,25.804 27.688,24.7C25.924,23.954 24.771,22.216 24.769,20.301Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path id="_Path_2_" serif:id="_Path_2" d="M22.037,12.211C22.848,11.237 23.249,9.984 23.152,8.72C21.912,8.85 20.766,9.443 19.944,10.38C19.132,11.304 18.721,12.514 18.8,13.741C20.056,13.754 21.25,13.189 22.037,12.211Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M42.302,27.14L37.569,27.14L36.432,30.496L34.427,30.496L38.911,18.078L40.994,18.078L45.477,30.496L43.438,30.496L42.302,27.14ZM38.059,25.591L41.811,25.591L39.961,20.144L39.91,20.144L38.059,25.591Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M55.16,25.97C55.16,28.783 53.654,30.591 51.381,30.591C50.209,30.652 49.099,30.035 48.533,29.007L48.49,29.007L48.49,33.491L46.631,33.491L46.631,21.442L48.43,21.442L48.43,22.948L48.464,22.948C49.056,21.926 50.166,21.31 51.347,21.348C53.645,21.348 55.16,23.164 55.16,25.97ZM53.25,25.97C53.25,24.137 52.302,22.932 50.857,22.932C49.437,22.932 48.482,24.162 48.482,25.97C48.482,27.794 49.437,29.016 50.857,29.016C52.302,29.016 53.25,27.819 53.25,25.97Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M65.125,25.97C65.125,28.783 63.619,30.591 61.346,30.591C60.174,30.652 59.064,30.035 58.498,29.007L58.455,29.007L58.455,33.491L56.596,33.491L56.596,21.442L58.395,21.442L58.395,22.948L58.429,22.948C59.021,21.926 60.131,21.31 61.312,21.348C63.61,21.348 65.125,23.164 65.125,25.97ZM63.214,25.97C63.214,24.137 62.267,22.932 60.822,22.932C59.402,22.932 58.447,24.162 58.447,25.97C58.447,27.794 59.402,29.016 60.822,29.016C62.267,29.016 63.214,27.819 63.214,25.97L63.214,25.97Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M71.71,27.036C71.848,28.268 73.044,29.076 74.679,29.076C76.246,29.076 77.373,28.268 77.373,27.157C77.373,26.193 76.693,25.616 75.084,25.221L73.474,24.833C71.194,24.282 70.135,23.216 70.135,21.485C70.135,19.343 72.002,17.871 74.654,17.871C77.278,17.871 79.077,19.343 79.137,21.485L77.261,21.485C77.149,20.246 76.125,19.498 74.627,19.498C73.13,19.498 72.106,20.255 72.106,21.356C72.106,22.234 72.76,22.751 74.361,23.146L75.729,23.482C78.277,24.085 79.335,25.108 79.335,26.925C79.335,29.248 77.485,30.703 74.542,30.703C71.788,30.703 69.928,29.282 69.808,27.036L71.71,27.036Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M83.346,19.3L83.346,21.442L85.068,21.442L85.068,22.914L83.346,22.914L83.346,27.905C83.346,28.681 83.691,29.042 84.448,29.042C84.652,29.038 84.856,29.024 85.059,28.999L85.059,30.462C84.719,30.525 84.373,30.554 84.027,30.548C82.194,30.548 81.479,29.859 81.479,28.103L81.479,22.914L80.163,22.914L80.163,21.442L81.479,21.442L81.479,19.3L83.346,19.3Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M86.065,25.97C86.065,23.121 87.743,21.331 90.359,21.331C92.984,21.331 94.654,23.121 94.654,25.97C94.654,28.826 92.993,30.608 90.359,30.608C87.726,30.608 86.065,28.826 86.065,25.97ZM92.76,25.97C92.76,24.016 91.865,22.862 90.359,22.862C88.853,22.862 87.959,24.024 87.959,25.97C87.959,27.932 88.853,29.076 90.359,29.076C91.865,29.076 92.76,27.932 92.76,25.97L92.76,25.97Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M96.186,21.442L97.959,21.442L97.959,22.983L98.002,22.983C98.248,21.995 99.161,21.309 100.179,21.348C100.393,21.347 100.607,21.37 100.816,21.417L100.816,23.155C100.546,23.073 100.264,23.035 99.981,23.043C99.956,23.042 99.93,23.041 99.905,23.041C98.878,23.041 98.032,23.887 98.032,24.914C98.032,24.985 98.036,25.056 98.044,25.126L98.044,30.496L96.186,30.496L96.186,21.442Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M109.384,27.837C109.134,29.48 107.534,30.608 105.486,30.608C102.852,30.608 101.217,28.844 101.217,26.013C101.217,23.173 102.861,21.331 105.408,21.331C107.913,21.331 109.488,23.052 109.488,25.797L109.488,26.434L103.093,26.434L103.093,26.546C103.087,26.615 103.084,26.684 103.084,26.754C103.084,28.047 104.149,29.112 105.442,29.112C105.471,29.112 105.5,29.111 105.529,29.11C106.428,29.195 107.282,28.675 107.62,27.837L109.384,27.837ZM103.102,25.135L107.628,25.135C107.631,25.094 107.632,25.054 107.632,25.014C107.632,23.819 106.649,22.836 105.455,22.836C105.439,22.836 105.423,22.837 105.408,22.837C105.403,22.837 105.399,22.837 105.394,22.837C104.137,22.837 103.102,23.872 103.102,25.129C103.102,25.131 103.102,25.133 103.102,25.135L103.102,25.135Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_Group_4_" serif:id="_Group_4">
|
||||
<g>
|
||||
<path d="M37.826,8.731C37.889,8.726 37.952,8.724 38.015,8.724C39.463,8.724 40.655,9.916 40.655,11.364C40.655,11.475 40.648,11.586 40.634,11.696C40.634,13.602 39.604,14.698 37.826,14.698L35.671,14.698L35.671,8.731L37.826,8.731ZM36.598,13.854L37.723,13.854C37.76,13.856 37.797,13.857 37.835,13.857C38.864,13.857 39.71,13.011 39.71,11.981C39.71,11.89 39.704,11.799 39.69,11.708C39.702,11.623 39.708,11.538 39.708,11.452C39.708,10.42 38.859,9.571 37.827,9.571C37.792,9.571 37.757,9.572 37.723,9.574L36.598,9.574L36.598,13.854Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M41.681,12.444C41.674,12.377 41.671,12.309 41.671,12.241C41.671,11.071 42.634,10.108 43.804,10.108C44.974,10.108 45.937,11.071 45.937,12.241C45.937,12.309 45.934,12.377 45.928,12.444C45.934,12.513 45.938,12.582 45.938,12.651C45.938,13.822 44.975,14.785 43.804,14.785C42.634,14.785 41.671,13.822 41.671,12.651C41.671,12.582 41.674,12.513 41.681,12.444ZM45.014,12.444C45.014,11.468 44.575,10.897 43.806,10.897C43.033,10.897 42.599,11.468 42.599,12.444C42.599,13.428 43.033,13.995 43.806,13.995C44.575,13.995 45.014,13.424 45.014,12.444L45.014,12.444Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M51.573,14.698L50.651,14.698L49.721,11.381L49.65,11.381L48.724,14.698L47.811,14.698L46.569,10.195L47.471,10.195L48.277,13.631L48.344,13.631L49.27,10.195L50.122,10.195L51.048,13.631L51.118,13.631L51.921,10.195L52.81,10.195L51.573,14.698Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M53.854,10.195L54.709,10.195L54.709,10.91L54.775,10.91C55.006,10.385 55.547,10.061 56.119,10.108C56.156,10.105 56.192,10.104 56.229,10.104C57.032,10.104 57.694,10.765 57.694,11.568C57.694,11.64 57.688,11.712 57.678,11.783L57.678,14.698L56.789,14.698L56.789,12.006C56.789,11.282 56.475,10.922 55.817,10.922C55.801,10.922 55.785,10.921 55.769,10.921C55.203,10.921 54.736,11.388 54.736,11.954C54.736,11.991 54.738,12.027 54.742,12.063L54.742,14.698L53.854,14.698L53.854,10.195Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<rect x="59.094" y="8.437" width="0.889" height="6.261" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M61.218,12.444C61.211,12.377 61.208,12.309 61.208,12.241C61.208,11.071 62.171,10.108 63.342,10.108C64.512,10.108 65.475,11.071 65.475,12.241C65.475,12.309 65.472,12.377 65.465,12.444C65.472,12.513 65.475,12.582 65.475,12.651C65.475,13.821 64.512,14.785 63.342,14.785C62.171,14.785 61.208,13.821 61.208,12.651C61.208,12.582 61.211,12.513 61.218,12.444ZM64.551,12.444C64.551,11.468 64.112,10.897 63.343,10.897C62.57,10.897 62.136,11.468 62.136,12.444C62.136,13.428 62.57,13.995 63.343,13.995C64.112,13.995 64.551,13.424 64.551,12.444L64.551,12.444Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M66.401,13.424C66.401,12.614 67.004,12.146 68.076,12.08L69.295,12.01L69.295,11.621C69.295,11.146 68.981,10.877 68.374,10.877C67.877,10.877 67.534,11.059 67.435,11.377L66.575,11.377C66.666,10.604 67.393,10.108 68.415,10.108C69.543,10.108 70.18,10.67 70.18,11.621L70.18,14.698L69.325,14.698L69.325,14.065L69.254,14.065C68.964,14.526 68.446,14.797 67.902,14.772C67.855,14.777 67.808,14.779 67.761,14.779C67.017,14.779 66.404,14.169 66.401,13.424ZM69.295,13.04L69.295,12.663L68.196,12.733C67.576,12.775 67.294,12.986 67.294,13.383C67.294,13.788 67.646,14.024 68.129,14.024C68.165,14.028 68.201,14.029 68.236,14.029C68.792,14.029 69.258,13.594 69.295,13.04Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M71.348,12.444C71.348,11.021 72.08,10.12 73.217,10.12C73.791,10.094 74.33,10.402 74.598,10.91L74.665,10.91L74.665,8.437L75.553,8.437L75.553,14.698L74.702,14.698L74.702,13.986L74.631,13.986C74.343,14.49 73.797,14.793 73.217,14.772C72.072,14.772 71.348,13.871 71.348,12.444ZM72.266,12.444C72.266,13.399 72.716,13.974 73.469,13.974C74.218,13.974 74.681,13.391 74.681,12.448C74.681,11.51 74.213,10.918 73.469,10.918C72.721,10.918 72.266,11.497 72.266,12.444L72.266,12.444Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M79.23,12.444C79.224,12.377 79.22,12.309 79.22,12.241C79.22,11.071 80.183,10.108 81.354,10.108C82.524,10.108 83.487,11.071 83.487,12.241C83.487,12.309 83.484,12.377 83.477,12.444C83.484,12.513 83.487,12.582 83.487,12.651C83.487,13.822 82.524,14.785 81.354,14.785C80.183,14.785 79.22,13.822 79.22,12.651C79.22,12.582 79.223,12.513 79.23,12.444ZM82.563,12.444C82.563,11.468 82.125,10.897 81.355,10.897C80.583,10.897 80.148,11.468 80.148,12.444C80.148,13.428 80.583,13.995 81.355,13.995C82.125,13.995 82.563,13.424 82.563,12.444Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M84.669,10.195L85.525,10.195L85.525,10.91L85.591,10.91C85.822,10.385 86.363,10.061 86.935,10.108C86.972,10.105 87.008,10.104 87.045,10.104C87.848,10.104 88.509,10.765 88.509,11.568C88.509,11.64 88.504,11.712 88.494,11.783L88.494,14.698L87.605,14.698L87.605,12.006C87.605,11.282 87.291,10.922 86.633,10.922C86.617,10.922 86.601,10.921 86.585,10.921C86.019,10.921 85.552,11.388 85.552,11.954C85.552,11.991 85.554,12.027 85.558,12.063L85.558,14.698L84.669,14.698L84.669,10.195Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M93.515,9.074L93.515,10.215L94.491,10.215L94.491,10.964L93.515,10.964L93.515,13.279C93.515,13.751 93.709,13.958 94.152,13.958C94.265,13.957 94.378,13.95 94.491,13.937L94.491,14.677C94.331,14.706 94.169,14.721 94.007,14.723C93.019,14.723 92.626,14.375 92.626,13.507L92.626,10.964L91.911,10.964L91.911,10.215L92.626,10.215L92.626,9.074L93.515,9.074Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M95.705,8.437L96.585,8.437L96.585,10.918L96.656,10.918C96.897,10.388 97.448,10.065 98.029,10.112C98.056,10.11 98.082,10.11 98.109,10.11C98.923,10.11 99.593,10.779 99.593,11.593C99.593,11.659 99.588,11.725 99.58,11.791L99.58,14.698L98.69,14.698L98.69,12.01C98.69,11.291 98.355,10.926 97.727,10.926C97.699,10.924 97.67,10.923 97.641,10.923C97.064,10.923 96.589,11.398 96.589,11.975C96.589,12.006 96.591,12.037 96.593,12.068L96.593,14.698L95.705,14.698L95.705,8.437Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M104.761,13.482C104.513,14.328 103.687,14.88 102.81,14.785C102.792,14.785 102.774,14.785 102.756,14.785C101.634,14.785 100.711,13.862 100.711,12.74C100.711,12.647 100.717,12.553 100.73,12.46C100.718,12.369 100.712,12.277 100.712,12.185C100.712,11.045 101.649,10.108 102.788,10.108C102.794,10.108 102.8,10.108 102.806,10.108C104.059,10.108 104.815,10.964 104.815,12.378L104.815,12.688L101.635,12.688L101.635,12.738C101.632,12.771 101.631,12.804 101.631,12.838C101.631,13.491 102.168,14.028 102.821,14.028C102.826,14.028 102.83,14.028 102.834,14.028C103.268,14.08 103.693,13.864 103.906,13.482L104.761,13.482ZM101.635,12.031L103.91,12.031C103.912,12.004 103.913,11.977 103.913,11.95C103.913,11.354 103.422,10.864 102.826,10.864C102.818,10.864 102.81,10.864 102.801,10.864C102.796,10.864 102.792,10.864 102.787,10.864C102.155,10.864 101.635,11.384 101.635,12.016C101.635,12.021 101.635,12.026 101.635,12.031L101.635,12.031Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 16 KiB |
BIN
.github/assets/badge-desktop.png
vendored
|
Before Width: | Height: | Size: 19 KiB |
27
.github/assets/badge-flathub.svg
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 300 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;">
|
||||
<g transform="matrix(1,0,0,1,-348.1,-151.685)">
|
||||
<path d="M646.159,168.627C646.159,159.796 638.99,152.627 630.159,152.627L365.042,152.627C356.211,152.627 349.042,159.796 349.042,168.627L349.042,234.41C349.042,243.241 356.211,250.41 365.042,250.41L630.159,250.41C638.99,250.41 646.159,243.241 646.159,234.41L646.159,168.627Z" style="stroke:rgb(136,138,133);stroke-width:1.88px;"/>
|
||||
<g>
|
||||
<path d="M464.084,188.646C467.784,188.646 469.925,186.351 469.925,182.556C469.925,178.778 467.785,176.501 464.209,176.501L460.153,176.501L460.153,188.646L464.084,188.646ZM461.985,187.045L461.985,178.102L464.096,178.102C466.753,178.102 468.123,179.692 468.123,182.556C468.123,185.432 466.753,187.045 463.983,187.045L461.985,187.045ZM478.644,188.83C481.212,188.83 482.89,186.95 482.89,184.133C482.89,181.299 481.212,179.419 478.644,179.419C476.077,179.419 474.398,181.299 474.398,184.133C474.398,186.95 476.077,188.83 478.644,188.83ZM478.65,187.341C476.972,187.341 476.19,185.877 476.19,184.127C476.19,182.384 476.972,180.901 478.65,180.901C480.317,180.901 481.1,182.384 481.1,184.127C481.1,185.877 480.317,187.341 478.65,187.341ZM489.308,188.646L491.111,188.646L492.961,182.07L493.097,182.07L494.947,188.646L496.757,188.646L499.431,179.537L497.598,179.537L495.825,186.197L495.736,186.197L493.957,179.537L492.125,179.537L490.334,186.227L490.245,186.227L488.46,179.537L486.628,179.537L489.308,188.646ZM505.63,183.238C505.63,181.785 506.519,180.955 507.752,180.955C508.956,180.955 509.685,181.743 509.685,183.065L509.685,188.645L511.458,188.645L511.458,182.852C511.458,180.599 510.219,179.419 508.358,179.419C506.988,179.419 506.092,180.053 505.671,181.019L505.558,181.019L505.558,179.537L503.856,179.537L503.856,188.646L505.629,188.646L505.63,183.238ZM518.35,176.501L516.578,176.501L516.578,188.646L518.351,188.646L518.35,176.501ZM527.319,188.83C529.887,188.83 531.565,186.95 531.565,184.133C531.565,181.299 529.887,179.419 527.319,179.419C524.751,179.419 523.073,181.299 523.073,184.133C523.073,186.95 524.751,188.83 527.319,188.83ZM527.325,187.341C525.647,187.341 524.864,185.877 524.864,184.127C524.864,182.384 525.647,180.901 527.324,180.901C528.991,180.901 529.774,182.384 529.774,184.127C529.774,185.877 528.991,187.341 527.324,187.341L527.325,187.341ZM538.937,188.848C540.444,188.848 541.292,188.083 541.63,187.401L541.7,187.401L541.7,188.646L543.432,188.646L543.432,182.597C543.432,179.947 541.345,179.419 539.898,179.419C538.25,179.419 536.732,180.083 536.138,181.743L537.805,182.123C538.065,181.477 538.73,180.853 539.922,180.853C541.066,180.853 541.653,181.453 541.653,182.485L541.653,182.526C541.653,183.173 540.989,183.161 539.353,183.35C537.627,183.552 535.86,184.003 535.86,186.072C535.86,187.863 537.206,188.848 538.938,188.848L538.937,188.848ZM539.323,187.424C538.321,187.424 537.597,186.974 537.597,186.096C537.597,185.147 538.439,184.809 539.465,184.673C540.04,184.596 541.405,184.442 541.659,184.187L541.659,185.361C541.659,186.44 540.799,187.424 539.323,187.424ZM551.95,188.824C553.61,188.824 554.262,187.81 554.583,187.229L554.731,187.229L554.731,188.646L556.462,188.646L556.462,176.5L554.689,176.5L554.689,181.013L554.583,181.013C554.263,180.45 553.658,179.418 551.962,179.418C549.762,179.418 548.142,181.155 548.142,184.108C548.142,187.056 549.738,188.823 551.95,188.823L551.95,188.824ZM552.341,187.312C550.758,187.312 549.933,185.918 549.933,184.092C549.933,182.283 550.74,180.925 552.341,180.925C553.889,180.925 554.719,182.188 554.719,184.092C554.719,186.007 553.871,187.312 552.341,187.312ZM572.744,188.83C575.312,188.83 576.99,186.95 576.99,184.133C576.99,181.299 575.312,179.419 572.744,179.419C570.176,179.419 568.498,181.299 568.498,184.133C568.498,186.95 570.176,188.83 572.744,188.83ZM572.75,187.341C571.072,187.341 570.289,185.877 570.289,184.127C570.289,182.384 571.072,180.901 572.749,180.901C574.416,180.901 575.199,182.384 575.199,184.127C575.199,185.877 574.416,187.341 572.749,187.341L572.75,187.341ZM583.485,183.238C583.485,181.785 584.375,180.955 585.608,180.955C586.812,180.955 587.541,181.743 587.541,183.065L587.541,188.645L589.314,188.645L589.314,182.852C589.314,180.599 588.074,179.419 586.213,179.419C584.843,179.419 583.947,180.053 583.526,181.019L583.414,181.019L583.414,179.537L581.712,179.537L581.712,188.646L583.485,188.646L583.485,183.238Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M460.153,222.645L464.983,222.645L464.983,211.32L476.06,211.32L476.06,207.272L464.982,207.272L464.982,200.034L477.232,200.034L477.232,195.986L460.152,195.986L460.153,222.645Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<rect x="481.41" y="195.986" width="4.712" height="26.66" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M496.718,223.049C499.855,223.049 501.73,221.578 502.588,219.899L502.745,219.899L502.745,222.645L507.275,222.645L507.275,209.263C507.275,203.978 502.966,202.39 499.152,202.39C494.948,202.39 491.719,204.265 490.678,207.91L495.078,208.535C495.546,207.168 496.874,205.996 499.178,205.996C501.365,205.996 502.563,207.116 502.563,209.081L502.563,209.159C502.563,210.513 501.143,210.579 497.616,210.956C493.737,211.372 490.027,212.531 490.027,217.035C490.027,220.965 492.904,223.049 496.718,223.049ZM497.942,219.586C495.976,219.586 494.57,218.688 494.57,216.956C494.57,215.147 496.145,214.392 498.254,214.093C499.491,213.923 501.964,213.611 502.576,213.117L502.576,215.473C502.576,217.699 500.779,219.586 497.942,219.586Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M522.01,202.65L518.066,202.65L518.066,197.86L513.354,197.86L513.354,202.65L510.516,202.65L510.516,206.296L513.354,206.296L513.354,217.412C513.328,221.174 516.062,223.022 519.602,222.918C520.943,222.879 521.867,222.619 522.375,222.45L521.581,218.766C521.321,218.831 520.787,218.948 520.201,218.948C519.016,218.948 518.066,218.532 518.066,216.631L518.066,206.296L522.01,206.296L522.01,202.65Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M531.083,210.93C531.083,208.04 532.88,206.374 535.405,206.374C537.878,206.374 539.336,207.949 539.336,210.644L539.336,222.645L544.049,222.645L544.049,209.915C544.049,205.085 541.315,202.39 537.162,202.39C534.09,202.39 532.138,203.783 531.214,206.048L530.979,206.048L530.979,195.986L526.371,195.986L526.371,222.646L531.083,222.646L531.083,210.93Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M561.583,214.236C561.583,217.282 559.409,218.792 557.326,218.792C555.061,218.792 553.551,217.191 553.551,214.652L553.551,202.651L548.839,202.651L548.839,215.381C548.839,220.185 551.572,222.905 555.504,222.905C558.498,222.905 560.606,221.33 561.518,219.091L561.726,219.091L561.726,222.645L566.295,222.645L566.295,202.651L561.583,202.651L561.583,214.236Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M571.346,222.645L575.98,222.645L575.98,219.495L576.253,219.495C576.995,220.953 578.544,222.997 581.981,222.997C586.693,222.997 590.221,219.261 590.221,212.674C590.221,206.009 586.589,202.39 581.968,202.39C578.44,202.39 576.969,204.512 576.253,205.957L576.058,205.957L576.058,195.986L571.346,195.986L571.346,222.645ZM575.966,212.648C575.966,208.768 577.633,206.256 580.666,206.256C583.803,206.256 585.417,208.925 585.417,212.648C585.417,216.397 583.777,219.13 580.666,219.13C577.659,219.13 575.966,216.527 575.966,212.648Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.24662,0,0,1.24662,192.661,67.896)">
|
||||
<circle cx="166.688" cy="95.647" r="9.048" style="fill:white;"/>
|
||||
<path d="M174.826,111.309C174.826,109.371 173.253,107.798 171.315,107.798L161.925,107.798C159.987,107.798 158.414,109.371 158.414,111.309L158.414,120.699C158.414,122.637 159.987,124.21 161.925,124.21L171.315,124.21C173.253,124.21 174.826,122.637 174.826,120.699L174.826,111.309Z" style="fill:white;"/>
|
||||
<g transform="matrix(0.925902,0.53457,0.53457,-0.925902,99.8266,110.693)">
|
||||
<path d="M69.514,58.833L57.486,58.833C56.614,58.833 55.805,58.366 55.369,57.611C54.933,56.856 54.933,55.922 55.369,55.167L56.259,53.625L61.383,44.75C61.819,43.994 62.628,43.527 63.5,43.527C64.372,43.527 65.181,43.994 65.617,44.75L66.507,46.292L71.631,55.167C72.067,55.922 72.067,56.856 71.631,57.611C71.195,58.366 70.386,58.833 69.514,58.833Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<path d="M194.992,116.11C194.992,116.99 194.284,117.697 193.404,117.697L180.704,117.697C179.824,117.697 179.117,116.989 179.117,116.11C179.117,115.23 179.825,114.522 180.704,114.522L193.404,114.522C194.284,114.522 194.992,115.23 194.992,116.11ZM187.054,108.172C187.934,108.172 188.642,108.88 188.642,109.76L188.642,122.46C188.642,123.34 187.934,124.047 187.054,124.047C186.174,124.047 185.467,123.339 185.467,122.46L185.467,109.76C185.467,108.88 186.175,108.172 187.054,108.172Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
BIN
.github/assets/badge-google-play.png
vendored
|
Before Width: | Height: | Size: 16 KiB |
25
.github/assets/badge-google-play.svg
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 563 167" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:2;">
|
||||
<g transform="matrix(4.16667,0,0,4.16667,0,-4)">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M130,41L4.999,41C2.249,41 0,38.75 0,36L0,6C0,3.25 2.249,1 4.999,1L130,1C132.75,1 135,3.25 135,6L135,36C135,38.75 132.75,41 130,41Z" style="fill:rgb(3,4,4);fill-rule:nonzero;"/>
|
||||
<path d="M130,1L4.999,1C2.249,1 0,3.25 0,6L0,36C0,38.75 2.249,41 4.999,41L130,41C132.75,41 135,38.75 135,36L135,6C135,3.25 132.75,1 130,1ZM130,1.8C132.316,1.8 134.2,3.685 134.2,6L134.2,36C134.2,38.316 132.316,40.201 130,40.201L4.999,40.201C2.684,40.201 0.8,38.316 0.8,36L0.8,6C0.8,3.685 2.684,1.8 4.999,1.8L130,1.8Z" style="fill:rgb(167,165,166);fill-rule:nonzero;"/>
|
||||
<path d="M47.376,10.791L44.468,10.791L44.468,11.512L46.647,11.512C46.588,12.098 46.353,12.559 45.96,12.894C45.566,13.229 45.063,13.397 44.468,13.397C43.814,13.397 43.261,13.171 42.809,12.718C42.365,12.257 42.138,11.688 42.138,11C42.138,10.313 42.365,9.743 42.809,9.282C43.261,8.83 43.814,8.604 44.468,8.604C44.803,8.604 45.122,8.662 45.415,8.788C45.708,8.914 45.943,9.09 46.127,9.316L46.68,8.763C46.429,8.478 46.11,8.26 45.717,8.101C45.323,7.942 44.912,7.866 44.468,7.866C43.596,7.866 42.859,8.168 42.256,8.771C41.652,9.375 41.351,10.12 41.351,11C41.351,11.88 41.652,12.626 42.256,13.229C42.859,13.833 43.596,14.134 44.468,14.134C45.381,14.134 46.11,13.841 46.672,13.246C47.166,12.752 47.418,12.081 47.418,11.243C47.418,11.101 47.401,10.95 47.376,10.791Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M48.524,8L48.524,14L52.027,14L52.027,13.263L49.295,13.263L49.295,11.361L51.758,11.361L51.758,10.64L49.295,10.64L49.295,8.738L52.027,8.738L52.027,8L48.524,8Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M56.953,8.738L56.953,8L52.83,8L52.83,8.738L54.506,8.738L54.506,14L55.277,14L55.277,8.738L56.953,8.738Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<rect x="59.937" y="8" width="0.771" height="6" style="fill:white;"/>
|
||||
<path d="M65.803,8.738L65.803,8L61.68,8L61.68,8.738L63.356,8.738L63.356,14L64.127,14L64.127,8.738L65.803,8.738Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M73.605,8.78C73.01,8.168 72.281,7.866 71.409,7.866C70.538,7.866 69.808,8.168 69.213,8.771C68.618,9.366 68.325,10.112 68.325,11C68.325,11.889 68.618,12.634 69.213,13.229C69.808,13.833 70.538,14.134 71.409,14.134C72.272,14.134 73.01,13.833 73.605,13.229C74.2,12.634 74.493,11.889 74.493,11C74.493,10.12 74.2,9.375 73.605,8.78ZM69.767,9.282C70.211,8.83 70.755,8.604 71.409,8.604C72.063,8.604 72.607,8.83 73.043,9.282C73.487,9.727 73.705,10.305 73.705,11C73.705,11.696 73.487,12.274 73.043,12.718C72.607,13.171 72.063,13.397 71.409,13.397C70.755,13.397 70.211,13.171 69.767,12.718C69.331,12.266 69.113,11.696 69.113,11C69.113,10.305 69.331,9.735 69.767,9.282Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M76.345,10.263L76.312,9.106L76.345,9.106L79.396,14L80.2,14L80.2,8L79.429,8L79.429,11.512L79.463,12.668L79.429,12.668L76.513,8L75.575,8L75.575,14L76.345,14L76.345,10.263Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M47.376,10.791L44.468,10.791L44.468,11.512L46.647,11.512C46.588,12.098 46.353,12.559 45.96,12.894C45.566,13.229 45.063,13.397 44.468,13.397C43.814,13.397 43.261,13.171 42.809,12.718C42.365,12.257 42.138,11.688 42.138,11C42.138,10.313 42.365,9.743 42.809,9.282C43.261,8.83 43.814,8.604 44.468,8.604C44.803,8.604 45.122,8.662 45.415,8.788C45.708,8.914 45.943,9.09 46.127,9.316L46.68,8.763C46.429,8.478 46.11,8.26 45.717,8.101C45.323,7.942 44.912,7.866 44.468,7.866C43.596,7.866 42.859,8.168 42.256,8.771C41.652,9.375 41.351,10.12 41.351,11C41.351,11.88 41.652,12.626 42.256,13.229C42.859,13.833 43.596,14.134 44.468,14.134C45.381,14.134 46.11,13.841 46.672,13.246C47.166,12.752 47.418,12.081 47.418,11.243C47.418,11.101 47.401,10.95 47.376,10.791ZM48.524,8L48.524,14L52.027,14L52.027,13.263L49.295,13.263L49.295,11.361L51.758,11.361L51.758,10.64L49.295,10.64L49.295,8.738L52.027,8.738L52.027,8L48.524,8ZM56.953,8.738L56.953,8L52.831,8L52.831,8.738L54.507,8.738L54.507,14L55.277,14L55.277,8.738L56.953,8.738ZM60.708,8L59.937,8L59.937,14L60.708,14L60.708,8ZM65.803,8.738L65.803,8L61.68,8L61.68,8.738L63.356,8.738L63.356,14L64.127,14L64.127,8.738L65.803,8.738ZM73.605,8.78C73.01,8.168 72.281,7.866 71.409,7.866C70.537,7.866 69.808,8.168 69.213,8.771C68.618,9.366 68.325,10.112 68.325,11C68.325,11.889 68.618,12.634 69.213,13.229C69.808,13.833 70.537,14.134 71.409,14.134C72.272,14.134 73.01,13.833 73.605,13.229C74.2,12.634 74.493,11.889 74.493,11C74.493,10.12 74.2,9.375 73.605,8.78ZM69.767,9.282C70.211,8.83 70.755,8.604 71.409,8.604C72.063,8.604 72.607,8.83 73.043,9.282C73.487,9.727 73.705,10.305 73.705,11C73.705,11.696 73.487,12.274 73.043,12.718C72.607,13.171 72.063,13.397 71.409,13.397C70.755,13.397 70.211,13.171 69.767,12.718C69.331,12.266 69.113,11.696 69.113,11C69.113,10.305 69.331,9.735 69.767,9.282ZM76.346,10.263L76.312,9.106L76.346,9.106L79.396,14L80.2,14L80.2,8L79.429,8L79.429,11.512L79.463,12.668L79.429,12.668L76.513,8L75.575,8L75.575,14L76.346,14L76.346,10.263Z" style="fill:none;stroke:white;stroke-width:0.2px;"/>
|
||||
<path d="M106.936,31.001L108.802,31.001L108.802,18.499L106.936,18.499L106.936,31.001ZM123.743,23.002L121.604,28.422L121.54,28.422L119.32,23.002L117.31,23.002L120.64,30.578L118.741,34.791L120.687,34.791L125.818,23.002L123.743,23.002ZM113.16,29.581C112.55,29.581 111.697,29.275 111.697,28.519C111.697,27.554 112.759,27.184 113.675,27.184C114.495,27.184 114.882,27.361 115.38,27.602C115.235,28.76 114.238,29.581 113.16,29.581ZM113.386,22.729C112.035,22.729 110.636,23.324 110.057,24.643L111.713,25.334C112.067,24.643 112.726,24.418 113.418,24.418C114.383,24.418 115.364,24.997 115.38,26.026L115.38,26.155C115.042,25.962 114.318,25.672 113.434,25.672C111.648,25.672 109.831,26.653 109.831,28.487C109.831,30.16 111.295,31.237 112.935,31.237C114.189,31.237 114.882,30.674 115.315,30.015L115.38,30.015L115.38,30.98L117.182,30.98L117.182,26.187C117.182,23.967 115.524,22.729 113.386,22.729ZM101.854,24.524L99.2,24.524L99.2,20.239L101.854,20.239C103.249,20.239 104.041,21.394 104.041,22.382C104.041,23.351 103.249,24.524 101.854,24.524ZM101.806,18.499L97.334,18.499L97.334,31.001L99.2,31.001L99.2,26.264L101.806,26.264C103.874,26.264 105.907,24.767 105.907,22.382C105.907,19.997 103.874,18.499 101.806,18.499ZM77.424,29.583C76.135,29.583 75.056,28.503 75.056,27.022C75.056,25.523 76.135,24.428 77.424,24.428C78.697,24.428 79.696,25.523 79.696,27.022C79.696,28.503 78.697,29.583 77.424,29.583ZM79.567,23.703L79.502,23.703C79.083,23.203 78.278,22.752 77.263,22.752C75.136,22.752 73.187,24.621 73.187,27.022C73.187,29.406 75.136,31.258 77.263,31.258C78.278,31.258 79.083,30.807 79.502,30.292L79.567,30.292L79.567,30.904C79.567,32.531 78.697,33.401 77.295,33.401C76.152,33.401 75.443,32.58 75.153,31.887L73.526,32.563C73.993,33.691 75.233,35.077 77.295,35.077C79.487,35.077 81.339,33.788 81.339,30.646L81.339,23.01L79.567,23.01L79.567,23.703ZM82.628,31.001L84.497,31.001L84.497,18.499L82.628,18.499L82.628,31.001ZM87.251,26.876C87.204,25.233 88.525,24.396 89.475,24.396C90.216,24.396 90.844,24.766 91.054,25.297L87.251,26.876ZM93.051,25.459C92.697,24.508 91.618,22.752 89.411,22.752C87.219,22.752 85.399,24.476 85.399,27.005C85.399,29.39 87.204,31.258 89.62,31.258C91.569,31.258 92.697,30.066 93.165,29.374L91.714,28.407C91.231,29.116 90.571,29.583 89.62,29.583C88.669,29.583 87.993,29.148 87.558,28.294L93.245,25.942L93.051,25.459ZM47.743,24.057L47.743,25.861L52.061,25.861C51.932,26.876 51.594,27.617 51.078,28.133C50.45,28.761 49.467,29.454 47.743,29.454C45.085,29.454 43.007,27.312 43.007,24.653C43.007,21.995 45.085,19.852 47.743,19.852C49.177,19.852 50.224,20.416 50.998,21.141L52.27,19.868C51.191,18.837 49.757,18.048 47.743,18.048C44.102,18.048 41.041,21.012 41.041,24.653C41.041,28.294 44.102,31.258 47.743,31.258C49.708,31.258 51.191,30.614 52.351,29.406C53.543,28.213 53.914,26.538 53.914,25.185C53.914,24.766 53.881,24.379 53.817,24.057L47.743,24.057ZM58.822,29.583C57.533,29.583 56.421,28.52 56.421,27.005C56.421,25.475 57.533,24.428 58.822,24.428C60.111,24.428 61.223,25.475 61.223,27.005C61.223,28.52 60.111,29.583 58.822,29.583ZM58.822,22.752C56.47,22.752 54.553,24.541 54.553,27.005C54.553,29.454 56.47,31.258 58.822,31.258C61.174,31.258 63.091,29.454 63.091,27.005C63.091,24.541 61.174,22.752 58.822,22.752ZM68.135,29.583C66.847,29.583 65.735,28.52 65.735,27.005C65.735,25.475 66.847,24.428 68.135,24.428C69.424,24.428 70.536,25.475 70.536,27.005C70.536,28.52 69.424,29.583 68.135,29.583ZM68.135,22.752C65.783,22.752 63.866,24.541 63.866,27.005C63.866,29.454 65.783,31.258 68.135,31.258C70.488,31.258 72.405,29.454 72.405,27.005C72.405,24.541 70.488,22.752 68.135,22.752Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M20.717,20.425L10.07,31.725C10.071,31.727 10.071,31.729 10.072,31.731C10.398,32.959 11.519,33.862 12.849,33.862C13.381,33.862 13.88,33.718 14.308,33.466L14.342,33.446L26.326,26.531L20.717,20.425Z" style="fill:rgb(235,67,53);fill-rule:nonzero;"/>
|
||||
<path d="M31.488,18.501L31.478,18.494L26.304,15.494L20.475,20.681L26.324,26.529L31.471,23.56C32.373,23.073 32.985,22.122 32.985,21.025C32.985,19.936 32.381,18.989 31.488,18.501Z" style="fill:rgb(250,188,19);fill-rule:nonzero;"/>
|
||||
<path d="M10.07,10.278C10.006,10.514 9.972,10.761 9.972,11.018L9.972,30.985C9.972,31.242 10.005,31.49 10.07,31.725L21.083,20.714L10.07,10.278Z" style="fill:rgb(84,125,191);fill-rule:nonzero;"/>
|
||||
<path d="M20.795,21.001L26.306,15.492L14.335,8.552C13.9,8.291 13.393,8.141 12.849,8.141C11.519,8.141 10.397,9.046 10.07,10.275L10.07,10.278L20.795,21.001Z" style="fill:rgb(48,168,81);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
52
.github/assets/badge-mac-app-store.svg
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 157 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M146.571,0L9.535,0C9.168,0 8.806,0 8.44,0.002C8.134,0.004 7.83,0.01 7.521,0.015C6.85,0.023 6.18,0.082 5.517,0.191C4.856,0.304 4.215,0.515 3.616,0.818C3.019,1.124 2.472,1.522 1.998,1.997C1.52,2.471 1.122,3.018 0.819,3.618C0.515,4.217 0.305,4.859 0.194,5.521C0.083,6.183 0.023,6.853 0.015,7.523C0.006,7.83 0.005,8.138 0,8.444L0,31.559C0.005,31.869 0.006,32.17 0.015,32.481C0.023,33.151 0.083,33.821 0.194,34.482C0.304,35.145 0.515,35.788 0.819,36.387C1.122,36.985 1.52,37.53 1.998,38.001C2.471,38.478 3.017,38.876 3.616,39.18C4.215,39.484 4.855,39.697 5.517,39.811C6.18,39.919 6.85,39.978 7.521,39.987C7.83,39.994 8.134,39.998 8.44,39.998C8.806,40 9.168,40 9.535,40L146.571,40C146.931,40 147.296,40 147.655,39.998C147.96,39.998 148.272,39.994 148.577,39.987C149.247,39.979 149.916,39.92 150.577,39.81C151.241,39.696 151.884,39.483 152.485,39.18C153.084,38.876 153.63,38.478 154.103,38.001C154.579,37.528 154.977,36.983 155.284,36.387C155.586,35.787 155.795,35.145 155.903,34.482C156.015,33.821 156.077,33.151 156.089,32.481C156.093,32.17 156.093,31.869 156.093,31.559C156.101,31.195 156.101,30.834 156.101,30.465L156.101,9.536C156.101,9.17 156.101,8.807 156.093,8.444C156.093,8.138 156.093,7.83 156.089,7.523C156.077,6.852 156.015,6.183 155.903,5.521C155.795,4.859 155.586,4.217 155.284,3.618C154.667,2.415 153.688,1.436 152.485,0.818C151.884,0.516 151.241,0.304 150.577,0.191C149.916,0.081 149.247,0.022 148.577,0.015C148.272,0.01 147.96,0.004 147.655,0.002C147.296,-0 146.931,-0 146.571,-0L146.571,0Z" style="fill:rgb(166,166,166);fill-rule:nonzero;"/>
|
||||
<path d="M8.445,39.125C8.14,39.125 7.843,39.121 7.541,39.114C6.914,39.106 6.29,39.052 5.671,38.951C5.095,38.852 4.537,38.667 4.015,38.403C3.498,38.142 3.026,37.798 2.618,37.387C2.204,36.98 1.859,36.508 1.597,35.99C1.333,35.469 1.149,34.91 1.054,34.333C0.951,33.713 0.896,33.086 0.888,32.458C0.881,32.247 0.873,31.545 0.873,31.545L0.873,8.444C0.873,8.444 0.882,7.753 0.888,7.55C0.895,6.922 0.951,6.297 1.053,5.678C1.149,5.099 1.332,4.539 1.597,4.016C1.857,3.498 2.2,3.026 2.612,2.618C3.023,2.206 3.496,1.861 4.014,1.595C4.535,1.332 5.092,1.149 5.667,1.051C6.288,0.95 6.915,0.895 7.543,0.887L8.445,0.875L147.65,0.875L148.563,0.888C149.186,0.895 149.807,0.949 150.422,1.05C151.003,1.149 151.566,1.334 152.093,1.598C153.13,2.133 153.975,2.979 154.508,4.018C154.768,4.538 154.949,5.094 155.043,5.667C155.147,6.291 155.205,6.922 155.217,7.554C155.22,7.837 155.22,8.142 155.22,8.444C155.228,8.819 155.228,9.176 155.228,9.536L155.228,30.465C155.228,30.828 155.228,31.183 155.22,31.54C155.22,31.865 155.22,32.163 155.216,32.47C155.204,33.091 155.147,33.71 155.045,34.323C154.952,34.904 154.769,35.468 154.505,35.993C154.241,36.506 153.898,36.973 153.489,37.379C153.081,37.793 152.608,38.138 152.09,38.401C151.565,38.667 151.002,38.853 150.422,38.951C149.804,39.052 149.179,39.107 148.553,39.114C148.26,39.121 147.953,39.125 147.655,39.125L146.571,39.127L8.445,39.125Z" style="fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g id="_Group_">
|
||||
<g id="_Group_2_" serif:id="_Group_2">
|
||||
<g id="_Group_3_" serif:id="_Group_3">
|
||||
<g id="_Group_4_" serif:id="_Group_4">
|
||||
<path id="_Path_" d="M24.769,20.301C24.791,18.606 25.682,17.036 27.125,16.149C26.21,14.842 24.729,14.041 23.134,13.991C21.455,13.815 19.827,14.996 18.971,14.996C18.099,14.996 16.782,14.008 15.363,14.038C13.498,14.098 11.797,15.135 10.89,16.766C8.956,20.114 10.399,25.035 12.251,27.742C13.178,29.067 14.261,30.547 15.679,30.495C17.066,30.437 17.584,29.61 19.258,29.61C20.917,29.61 21.403,30.495 22.849,30.462C24.338,30.437 25.275,29.13 26.17,27.792C26.836,26.848 27.348,25.804 27.688,24.7C25.924,23.954 24.771,22.216 24.769,20.301Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path id="_Path_2_" serif:id="_Path_2" d="M22.037,12.211C22.848,11.237 23.249,9.984 23.152,8.72C21.912,8.85 20.766,9.443 19.944,10.38C19.132,11.304 18.721,12.514 18.8,13.741C20.056,13.754 21.25,13.189 22.037,12.211Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M46.149,30.496L46.149,21.356L46.088,21.356L42.345,30.401L40.917,30.401L37.164,21.356L37.104,21.356L37.104,30.496L35.348,30.496L35.348,18.078L37.578,18.078L41.596,27.88L41.665,27.88L45.675,18.078L47.913,18.078L47.913,30.496L46.149,30.496Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M49.396,27.923C49.396,26.34 50.609,25.384 52.761,25.255L55.24,25.117L55.24,24.429C55.24,23.422 54.577,22.853 53.449,22.853C52.588,22.739 51.768,23.289 51.547,24.128L49.809,24.128C49.861,22.492 51.384,21.331 53.501,21.331C55.661,21.331 57.089,22.51 57.089,24.291L57.089,30.496L55.308,30.496L55.308,29.007L55.265,29.007C54.685,30.034 53.587,30.666 52.408,30.651C52.319,30.66 52.23,30.664 52.14,30.664C50.636,30.664 49.398,29.427 49.396,27.923ZM55.24,27.105L55.24,26.408L53.01,26.546C51.9,26.615 51.272,27.097 51.272,27.871C51.272,28.663 51.926,29.18 52.924,29.18C52.973,29.183 53.022,29.185 53.071,29.185C54.227,29.185 55.191,28.261 55.24,27.105L55.24,27.105Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M64.893,24.558C64.726,23.536 63.791,22.805 62.759,22.889C61.331,22.889 60.384,24.085 60.384,25.97C60.384,27.897 61.339,29.059 62.776,29.059C63.795,29.161 64.729,28.443 64.893,27.433L66.683,27.433C66.453,29.367 64.699,30.787 62.759,30.608C60.177,30.608 58.491,28.844 58.491,25.97C58.491,23.155 60.177,21.331 62.742,21.331C64.696,21.165 66.451,22.608 66.667,24.558L64.893,24.558Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M78.759,27.14L74.026,27.14L72.889,30.496L70.884,30.496L75.368,18.078L77.451,18.078L81.934,30.496L79.895,30.496L78.759,27.14ZM74.516,25.591L78.268,25.591L76.419,20.144L76.367,20.144L74.516,25.591Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M91.617,25.97C91.617,28.783 90.111,30.591 87.838,30.591C86.666,30.652 85.556,30.035 84.99,29.007L84.947,29.007L84.947,33.491L83.088,33.491L83.088,21.442L84.887,21.442L84.887,22.948L84.921,22.948C85.513,21.926 86.623,21.31 87.804,21.348C90.102,21.348 91.617,23.164 91.617,25.97ZM89.707,25.97C89.707,24.137 88.759,22.932 87.314,22.932C85.894,22.932 84.939,24.162 84.939,25.97C84.939,27.794 85.894,29.016 87.314,29.016C88.759,29.016 89.707,27.819 89.707,25.97Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M101.582,25.97C101.582,28.783 100.076,30.591 97.803,30.591C96.631,30.652 95.521,30.035 94.955,29.007L94.912,29.007L94.912,33.491L93.053,33.491L93.053,21.442L94.852,21.442L94.852,22.948L94.886,22.948C95.478,21.926 96.588,21.31 97.769,21.348C100.067,21.348 101.582,23.164 101.582,25.97ZM99.671,25.97C99.671,24.137 98.724,22.932 97.279,22.932C95.859,22.932 94.904,24.162 94.904,25.97C94.904,27.794 95.859,29.016 97.279,29.016C98.724,29.016 99.671,27.819 99.671,25.97L99.671,25.97Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M108.168,27.036C108.305,28.268 109.502,29.076 111.136,29.076C112.703,29.076 113.83,28.268 113.83,27.157C113.83,26.193 113.15,25.616 111.541,25.221L109.931,24.833C107.651,24.282 106.592,23.216 106.592,21.485C106.592,19.343 108.459,17.871 111.111,17.871C113.735,17.871 115.534,19.343 115.594,21.485L113.718,21.485C113.606,20.246 112.582,19.498 111.084,19.498C109.587,19.498 108.563,20.255 108.563,21.356C108.563,22.234 109.217,22.751 110.818,23.146L112.186,23.482C114.734,24.085 115.792,25.108 115.792,26.925C115.792,29.248 113.942,30.703 110.999,30.703C108.245,30.703 106.385,29.282 106.265,27.036L108.168,27.036Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M119.803,19.3L119.803,21.442L121.525,21.442L121.525,22.914L119.803,22.914L119.803,27.905C119.803,28.681 120.148,29.042 120.905,29.042C121.109,29.038 121.313,29.024 121.516,28.999L121.516,30.462C121.176,30.525 120.83,30.554 120.484,30.548C118.651,30.548 117.936,29.859 117.936,28.103L117.936,22.914L116.62,22.914L116.62,21.442L117.936,21.442L117.936,19.3L119.803,19.3Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M122.521,25.97C122.521,23.121 124.199,21.331 126.815,21.331C129.44,21.331 131.11,23.121 131.11,25.97C131.11,28.826 129.449,30.608 126.815,30.608C124.182,30.608 122.521,28.826 122.521,25.97ZM129.216,25.97C129.216,24.016 128.321,22.862 126.815,22.862C125.309,22.862 124.414,24.024 124.414,25.97C124.414,27.932 125.309,29.076 126.815,29.076C128.321,29.076 129.216,27.932 129.216,25.97L129.216,25.97Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M132.643,21.442L134.416,21.442L134.416,22.983L134.459,22.983C134.705,21.995 135.618,21.309 136.636,21.348C136.85,21.347 137.064,21.37 137.273,21.417L137.273,23.155C137.003,23.073 136.721,23.035 136.438,23.043C136.413,23.042 136.387,23.041 136.362,23.041C135.335,23.041 134.489,23.887 134.489,24.914C134.489,24.985 134.493,25.056 134.501,25.126L134.501,30.496L132.643,30.496L132.643,21.442Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M145.84,27.837C145.59,29.48 143.99,30.608 141.942,30.608C139.308,30.608 137.673,28.844 137.673,26.013C137.673,23.173 139.317,21.331 141.864,21.331C144.369,21.331 145.944,23.052 145.944,25.797L145.944,26.434L139.549,26.434L139.549,26.546C139.543,26.615 139.54,26.684 139.54,26.754C139.54,28.047 140.605,29.112 141.898,29.112C141.927,29.112 141.956,29.111 141.985,29.11C142.884,29.195 143.738,28.675 144.076,27.837L145.84,27.837ZM139.558,25.135L144.084,25.135C144.087,25.094 144.088,25.054 144.088,25.014C144.088,23.819 143.105,22.836 141.911,22.836C141.895,22.836 141.879,22.837 141.864,22.837C141.859,22.837 141.855,22.837 141.85,22.837C140.593,22.837 139.558,23.872 139.558,25.129C139.558,25.131 139.558,25.133 139.558,25.135L139.558,25.135Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_Group_5_" serif:id="_Group_5">
|
||||
<g>
|
||||
<path d="M37.826,8.731C37.889,8.726 37.952,8.724 38.015,8.724C39.463,8.724 40.655,9.916 40.655,11.364C40.655,11.475 40.648,11.586 40.634,11.696C40.634,13.602 39.604,14.698 37.826,14.698L35.671,14.698L35.671,8.731L37.826,8.731ZM36.598,13.854L37.723,13.854C37.76,13.856 37.797,13.857 37.835,13.857C38.864,13.857 39.71,13.011 39.71,11.981C39.71,11.89 39.704,11.799 39.69,11.708C39.702,11.623 39.708,11.538 39.708,11.452C39.708,10.42 38.859,9.571 37.827,9.571C37.792,9.571 37.757,9.572 37.723,9.574L36.598,9.574L36.598,13.854Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M41.681,12.444C41.674,12.377 41.671,12.309 41.671,12.241C41.671,11.071 42.634,10.108 43.804,10.108C44.974,10.108 45.937,11.071 45.937,12.241C45.937,12.309 45.934,12.377 45.928,12.444C45.934,12.513 45.938,12.582 45.938,12.651C45.938,13.822 44.975,14.785 43.804,14.785C42.634,14.785 41.671,13.822 41.671,12.651C41.671,12.582 41.674,12.513 41.681,12.444ZM45.014,12.444C45.014,11.468 44.575,10.897 43.806,10.897C43.033,10.897 42.599,11.468 42.599,12.444C42.599,13.428 43.033,13.995 43.806,13.995C44.575,13.995 45.014,13.424 45.014,12.444L45.014,12.444Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M51.573,14.698L50.651,14.698L49.721,11.381L49.65,11.381L48.724,14.698L47.811,14.698L46.569,10.195L47.471,10.195L48.277,13.631L48.344,13.631L49.27,10.195L50.122,10.195L51.048,13.631L51.118,13.631L51.921,10.195L52.81,10.195L51.573,14.698Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M53.854,10.195L54.709,10.195L54.709,10.91L54.775,10.91C55.006,10.385 55.547,10.061 56.119,10.108C56.156,10.105 56.192,10.104 56.229,10.104C57.032,10.104 57.694,10.765 57.694,11.568C57.694,11.64 57.688,11.712 57.678,11.783L57.678,14.698L56.789,14.698L56.789,12.006C56.789,11.282 56.475,10.922 55.817,10.922C55.801,10.922 55.785,10.921 55.769,10.921C55.203,10.921 54.736,11.388 54.736,11.954C54.736,11.991 54.738,12.027 54.742,12.063L54.742,14.698L53.854,14.698L53.854,10.195Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<rect x="59.094" y="8.437" width="0.889" height="6.261" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M61.218,12.444C61.211,12.377 61.208,12.309 61.208,12.241C61.208,11.071 62.171,10.108 63.341,10.108C64.512,10.108 65.475,11.071 65.475,12.241C65.475,12.309 65.471,12.377 65.465,12.444C65.472,12.513 65.475,12.582 65.475,12.651C65.475,13.822 64.512,14.785 63.341,14.785C62.171,14.785 61.208,13.822 61.208,12.651C61.208,12.582 61.211,12.513 61.218,12.444ZM64.551,12.444C64.551,11.468 64.112,10.897 63.343,10.897C62.57,10.897 62.136,11.468 62.136,12.444C62.136,13.428 62.57,13.995 63.343,13.995C64.112,13.995 64.551,13.424 64.551,12.444L64.551,12.444Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M66.4,13.424C66.4,12.614 67.004,12.146 68.075,12.08L69.295,12.01L69.295,11.621C69.295,11.146 68.98,10.877 68.373,10.877C67.877,10.877 67.533,11.059 67.435,11.377L66.574,11.377C66.665,10.604 67.393,10.108 68.414,10.108C69.543,10.108 70.18,10.67 70.18,11.621L70.18,14.698L69.324,14.698L69.324,14.065L69.254,14.065C68.964,14.526 68.446,14.797 67.901,14.772C67.855,14.777 67.808,14.779 67.761,14.779C67.016,14.779 66.403,14.169 66.4,13.424ZM69.295,13.04L69.295,12.663L68.195,12.733C67.575,12.775 67.294,12.986 67.294,13.383C67.294,13.788 67.646,14.024 68.129,14.024C68.164,14.028 68.2,14.029 68.236,14.029C68.791,14.029 69.257,13.594 69.295,13.04Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M71.348,12.444C71.348,11.021 72.079,10.12 73.217,10.12C73.79,10.094 74.33,10.402 74.598,10.91L74.664,10.91L74.664,8.437L75.553,8.437L75.553,14.698L74.701,14.698L74.701,13.986L74.631,13.986C74.343,14.49 73.797,14.793 73.217,14.772C72.071,14.772 71.348,13.871 71.348,12.444ZM72.266,12.444C72.266,13.399 72.716,13.974 73.469,13.974C74.218,13.974 74.681,13.391 74.681,12.448C74.681,11.51 74.213,10.918 73.469,10.918C72.721,10.918 72.266,11.497 72.266,12.444L72.266,12.444Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M79.23,12.444C79.223,12.377 79.22,12.309 79.22,12.241C79.22,11.071 80.183,10.108 81.353,10.108C82.524,10.108 83.487,11.071 83.487,12.241C83.487,12.309 83.484,12.377 83.477,12.444C83.484,12.513 83.487,12.582 83.487,12.651C83.487,13.821 82.524,14.785 81.353,14.785C80.183,14.785 79.219,13.821 79.219,12.651C79.219,12.582 79.223,12.513 79.23,12.444ZM82.563,12.444C82.563,11.468 82.124,10.897 81.355,10.897C80.582,10.897 80.148,11.468 80.148,12.444C80.148,13.428 80.582,13.995 81.355,13.995C82.124,13.995 82.563,13.424 82.563,12.444L82.563,12.444Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M84.669,10.195L85.525,10.195L85.525,10.91L85.591,10.91C85.822,10.385 86.363,10.061 86.935,10.108C86.972,10.105 87.008,10.104 87.045,10.104C87.848,10.104 88.509,10.765 88.509,11.568C88.509,11.64 88.504,11.712 88.494,11.783L88.494,14.698L87.605,14.698L87.605,12.006C87.605,11.282 87.291,10.922 86.633,10.922C86.617,10.922 86.601,10.921 86.585,10.921C86.019,10.921 85.552,11.388 85.552,11.954C85.552,11.991 85.554,12.027 85.558,12.063L85.558,14.698L84.669,14.698L84.669,10.195Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M93.515,9.074L93.515,10.215L94.491,10.215L94.491,10.964L93.515,10.964L93.515,13.279C93.515,13.751 93.709,13.958 94.152,13.958C94.265,13.957 94.378,13.95 94.491,13.937L94.491,14.677C94.331,14.706 94.169,14.721 94.007,14.723C93.019,14.723 92.626,14.375 92.626,13.507L92.626,10.964L91.911,10.964L91.911,10.215L92.626,10.215L92.626,9.074L93.515,9.074Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M95.705,8.437L96.585,8.437L96.585,10.918L96.656,10.918C96.897,10.388 97.448,10.065 98.029,10.112C98.056,10.11 98.082,10.11 98.109,10.11C98.923,10.11 99.593,10.779 99.593,11.593C99.593,11.659 99.588,11.725 99.58,11.791L99.58,14.698L98.69,14.698L98.69,12.01C98.69,11.291 98.355,10.926 97.727,10.926C97.699,10.924 97.67,10.923 97.641,10.923C97.064,10.923 96.589,11.398 96.589,11.975C96.589,12.006 96.591,12.037 96.593,12.068L96.593,14.698L95.705,14.698L95.705,8.437Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M104.761,13.482C104.513,14.328 103.687,14.88 102.81,14.785C102.792,14.785 102.774,14.785 102.756,14.785C101.634,14.785 100.711,13.862 100.711,12.74C100.711,12.647 100.717,12.553 100.73,12.46C100.718,12.369 100.712,12.277 100.712,12.185C100.712,11.045 101.649,10.108 102.788,10.108C102.794,10.108 102.8,10.108 102.806,10.108C104.059,10.108 104.815,10.964 104.815,12.378L104.815,12.688L101.635,12.688L101.635,12.738C101.632,12.771 101.631,12.804 101.631,12.838C101.631,13.491 102.168,14.028 102.821,14.028C102.826,14.028 102.83,14.028 102.834,14.028C103.268,14.08 103.693,13.864 103.906,13.482L104.761,13.482ZM101.635,12.031L103.91,12.031C103.912,12.004 103.913,11.977 103.913,11.95C103.913,11.354 103.422,10.864 102.826,10.864C102.818,10.864 102.81,10.864 102.801,10.864C102.796,10.864 102.792,10.864 102.787,10.864C102.155,10.864 101.635,11.384 101.635,12.016C101.635,12.021 101.635,12.026 101.635,12.031L101.635,12.031Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
23
.github/assets/badge-web.svg
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 135 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M130.2,40L4.73,40C2.137,39.995 0.005,37.863 -0,35.27L0,4.73C-0,2.135 2.135,0 4.73,0L130.2,0C132.807,-0 134.962,2.123 135,4.73L135,35.27C134.957,37.875 132.805,39.995 130.2,40Z" style="fill:rgb(166,166,166);fill-rule:nonzero;"/>
|
||||
<path d="M134,35.27C134,37.371 132.271,39.1 130.17,39.1L4.73,39.1C2.627,39.1 0.895,37.373 0.89,35.27L0.89,4.72C0.895,2.617 2.627,0.89 4.73,0.89L130.2,0.89C132.29,0.906 134,2.63 134,4.72L134,35.27Z" style="fill-rule:nonzero;"/>
|
||||
<path d="M40.56,25.26L41.82,22.08L45.45,22.08L43.73,17.26L45.88,11.81L52.05,28.19L47.5,28.19L46.5,25.26L40.56,25.26Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M36.68,28.19L43.29,11.81L38.91,11.81L34.39,22.4L31.18,11.81L27.81,11.81L24.36,22.4L21.93,17.57L19.73,24.35L22,28.19L26.31,28.19L29.43,18.7L32.43,28.19L36.68,28.19Z" style="fill:rgb(90,15,200);fill-rule:nonzero;"/>
|
||||
<path d="M12.7,22.57L15.4,22.57C16.136,22.582 16.871,22.488 17.58,22.29L18.28,20.14L20.23,14.14C20.08,13.902 19.91,13.678 19.72,13.47C18.57,12.302 16.965,11.695 15.33,11.81L8.55,11.81L8.55,28.19L12.7,28.19L12.7,22.57ZM16.27,15.57C16.674,15.994 16.888,16.565 16.86,17.15C16.892,17.723 16.706,18.288 16.34,18.73C15.77,19.23 15.014,19.466 14.26,19.38L12.7,19.38L12.7,15L14.27,15C14.987,14.922 15.706,15.13 16.27,15.58L16.27,15.57Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M61.25,14.45L61.25,8.74L61.91,8.74L61.91,13.85L64.43,13.85L64.43,14.45L61.25,14.45Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M67.94,14.45L67.81,13.84C67.645,14.069 67.43,14.258 67.18,14.39C66.925,14.489 66.653,14.536 66.38,14.53C66.017,14.55 65.66,14.432 65.38,14.2C65.131,13.957 64.999,13.617 65.02,13.27C65.02,12.4 65.71,11.95 67.1,11.91L67.82,11.91L67.82,11.64C67.842,11.376 67.768,11.113 67.61,10.9C67.421,10.725 67.167,10.637 66.91,10.66C66.489,10.68 66.076,10.789 65.7,10.98L65.5,10.49C65.719,10.366 65.955,10.275 66.2,10.22C66.444,10.152 66.697,10.119 66.95,10.12C67.359,10.087 67.766,10.208 68.09,10.46C68.358,10.757 68.492,11.151 68.46,11.55L68.46,14.47L67.94,14.45ZM66.47,14C66.836,14.03 67.198,13.906 67.47,13.66C67.712,13.413 67.839,13.075 67.82,12.73L67.82,12.35L67.17,12.35C66.783,12.339 66.399,12.422 66.05,12.59C65.827,12.738 65.697,12.993 65.71,13.26C65.696,13.459 65.773,13.654 65.92,13.79C66.075,13.918 66.269,13.992 66.47,14Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M70.36,10.17L70.36,13C70.337,13.281 70.423,13.56 70.6,13.78C70.801,13.964 71.068,14.058 71.34,14.04C71.712,14.077 72.081,13.94 72.34,13.67C72.576,13.317 72.685,12.893 72.65,12.47L72.65,10.17L73.3,10.17L73.3,14.45L72.76,14.45L72.67,13.88C72.539,14.092 72.348,14.259 72.12,14.36C71.866,14.476 71.589,14.534 71.31,14.53C70.887,14.564 70.467,14.431 70.14,14.16C69.841,13.85 69.682,13.43 69.7,13L69.7,10.2L70.36,10.17Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M77.59,14.45L77.59,11.68C77.613,11.399 77.527,11.12 77.35,10.9C77.149,10.716 76.882,10.622 76.61,10.64C76.238,10.603 75.869,10.74 75.61,11.01C75.37,11.361 75.26,11.786 75.3,12.21L75.3,14.45L74.65,14.45L74.65,10.17L75.18,10.17L75.28,10.76C75.41,10.545 75.602,10.374 75.83,10.27C76.079,10.148 76.353,10.087 76.63,10.09C77.055,10.059 77.475,10.195 77.8,10.47C78.09,10.795 78.232,11.226 78.19,11.66L78.19,14.45L77.59,14.45Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M81.28,14.53C80.746,14.573 80.219,14.379 79.84,14C79.474,13.543 79.292,12.965 79.33,12.38C79.294,11.782 79.479,11.191 79.85,10.72C80.231,10.314 80.774,10.098 81.33,10.13C81.535,10.134 81.739,10.157 81.94,10.2C82.11,10.23 82.275,10.284 82.43,10.36L82.23,10.91C82.077,10.848 81.92,10.798 81.76,10.76C81.612,10.731 81.461,10.714 81.31,10.71C80.44,10.71 80,11.26 80,12.37C79.974,12.797 80.087,13.221 80.32,13.58C80.56,13.881 80.937,14.04 81.32,14C81.698,13.998 82.072,13.92 82.42,13.77L82.42,14.35C82.061,14.504 81.669,14.566 81.28,14.53Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M86.31,14.45L86.31,11.68C86.333,11.399 86.247,11.12 86.07,10.9C85.869,10.716 85.602,10.622 85.33,10.64C84.958,10.608 84.591,10.744 84.33,11.01C84.093,11.363 83.983,11.786 84.02,12.21L84.02,14.45L83.37,14.45L83.37,8.37L84,8.37L84,10.21C84.013,10.393 84.013,10.577 84,10.76C84.135,10.556 84.321,10.39 84.54,10.28C84.792,10.157 85.07,10.096 85.35,10.1C85.776,10.068 86.199,10.2 86.53,10.47C86.846,10.783 87.017,11.215 87,11.66L87,14.45L86.31,14.45Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M93,14.45L92.87,13.84C92.704,14.073 92.485,14.261 92.23,14.39C91.979,14.49 91.71,14.537 91.44,14.53C91.077,14.552 90.719,14.434 90.44,14.2C90.191,13.957 90.059,13.617 90.08,13.27C90.08,12.4 90.77,11.95 92.15,11.91L92.88,11.91L92.88,11.64C92.909,11.374 92.829,11.107 92.66,10.9C92.476,10.723 92.224,10.635 91.97,10.66C91.548,10.678 91.135,10.787 90.76,10.98L90.56,10.49C90.776,10.368 91.008,10.277 91.25,10.22C91.498,10.152 91.753,10.119 92.01,10.12C92.416,10.084 92.821,10.206 93.14,10.46C93.413,10.754 93.547,11.151 93.51,11.55L93.51,14.47L93,14.45ZM91.54,14C91.889,14.016 92.23,13.894 92.49,13.66C92.73,13.412 92.853,13.074 92.83,12.73L92.83,12.35L92.19,12.35C91.803,12.339 91.419,12.422 91.07,12.59C90.844,12.735 90.713,12.992 90.73,13.26C90.716,13.459 90.793,13.654 90.94,13.79C91.108,13.931 91.321,14.005 91.54,14Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M97.58,13.28C97.596,13.646 97.426,13.996 97.13,14.21C96.757,14.444 96.32,14.556 95.88,14.53C95.424,14.554 94.97,14.461 94.56,14.26L94.56,13.66C94.974,13.876 95.433,13.993 95.9,14C96.171,14.015 96.44,13.956 96.68,13.83C96.858,13.732 96.966,13.543 96.96,13.34C96.957,13.171 96.876,13.012 96.74,12.91C96.477,12.731 96.191,12.59 95.89,12.49C95.581,12.391 95.283,12.26 95,12.1C94.851,12.006 94.724,11.879 94.63,11.73C94.542,11.575 94.497,11.399 94.5,11.22C94.516,10.88 94.705,10.57 95,10.4C95.346,10.173 95.757,10.064 96.17,10.09C96.634,10.094 97.093,10.189 97.52,10.37L97.29,10.9C96.921,10.736 96.524,10.647 96.12,10.64C95.881,10.621 95.642,10.669 95.43,10.78C95.282,10.858 95.189,11.013 95.19,11.18C95.188,11.284 95.219,11.386 95.28,11.47C95.355,11.566 95.451,11.645 95.56,11.7C95.802,11.825 96.052,11.932 96.31,12.02C96.68,12.133 97.021,12.324 97.31,12.58C97.487,12.769 97.584,13.021 97.58,13.28Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M71,28.45L69.23,28.45L67.53,22.45C67.46,22.19 67.37,21.82 67.26,21.33C67.15,20.84 67.08,20.5 67.06,20.33C67.06,20.63 66.94,21 66.84,21.47C66.74,21.94 66.65,22.28 66.58,22.47L64.93,28.4L63.15,28.4L61.86,23.4L60.54,18.4L62.21,18.4L63.64,24.22C63.86,25.16 64.02,25.98 64.12,26.7C64.12,26.31 64.25,25.88 64.35,25.4C64.45,24.92 64.54,24.53 64.62,24.24L66.25,18.4L67.87,18.4L69.53,24.26C69.733,25.084 69.89,25.919 70,26.76C70.105,25.92 70.269,25.088 70.49,24.27L72,18.46L73.66,18.46L71,28.45Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M78.15,28.59C77.134,28.657 76.135,28.294 75.4,27.59C74.701,26.819 74.34,25.799 74.4,24.76C74.341,23.707 74.668,22.669 75.32,21.84C75.965,21.125 76.898,20.735 77.86,20.78C78.74,20.735 79.598,21.066 80.22,21.69C80.83,22.378 81.143,23.282 81.09,24.2L81.09,25L76.09,25C76.077,25.621 76.291,26.225 76.69,26.7C77.11,27.111 77.684,27.325 78.27,27.29C78.69,27.294 79.11,27.251 79.52,27.16C79.947,27.065 80.363,26.924 80.76,26.74L80.76,28.05C80.382,28.231 79.982,28.362 79.57,28.44C79.103,28.538 78.627,28.588 78.15,28.59ZM77.86,22C77.399,21.978 76.95,22.157 76.63,22.49C76.296,22.886 76.102,23.382 76.08,23.9L79.51,23.9C79.537,23.388 79.377,22.883 79.06,22.48C78.75,22.152 78.311,21.976 77.86,22Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M86.78,20.75C87.632,20.705 88.454,21.079 88.98,21.75C89.557,22.595 89.835,23.609 89.77,24.63C89.835,25.656 89.553,26.674 88.97,27.52C88.417,28.176 87.602,28.555 86.745,28.555C85.888,28.555 85.073,28.176 84.52,27.52L84.42,27.52L84.12,28.41L82.92,28.41L82.92,17.82L84.52,17.82L84.52,21.88L84.58,21.88C85.073,21.154 85.903,20.728 86.78,20.75ZM86.36,22.07C85.829,22.023 85.307,22.235 84.96,22.64C84.627,23.219 84.474,23.884 84.52,24.55L84.52,24.66C84.467,25.355 84.62,26.051 84.96,26.66C85.301,27.094 85.84,27.327 86.39,27.28C86.913,27.301 87.409,27.042 87.69,26.6C88.026,25.99 88.179,25.295 88.13,24.6C88.13,22.93 87.54,22.07 86.36,22.07Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M101.65,28.45L100.65,25.68L96.83,25.68L95.83,28.45L94.13,28.45L97.87,18.45L99.65,18.45L103.38,28.45L101.65,28.45ZM100.22,24.27L99.28,21.55C99.21,21.37 99.12,21.08 99,20.69C98.88,20.3 98.8,20.01 98.75,19.83C98.612,20.452 98.431,21.063 98.21,21.66L97.31,24.27L100.22,24.27Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M108.37,28.59C107.509,28.641 106.675,28.267 106.14,27.59L106,27.59C106.07,28.22 106.1,28.59 106.1,28.75L106.1,31.85L104.49,31.85L104.49,20.89L105.79,20.89C105.79,21.03 105.9,21.37 106.02,21.89L106.1,21.89C106.592,21.131 107.456,20.691 108.36,20.74C109.211,20.698 110.032,21.071 110.56,21.74C111.137,22.585 111.415,23.599 111.35,24.62C111.409,25.645 111.128,26.661 110.55,27.51C110.04,28.199 109.227,28.601 108.37,28.59ZM108,22.07C107.471,22.022 106.95,22.23 106.6,22.63C106.264,23.163 106.11,23.792 106.16,24.42L106.16,24.66C106.107,25.355 106.26,26.051 106.6,26.66C106.939,27.097 107.479,27.331 108.03,27.28C108.549,27.298 109.04,27.034 109.31,26.59C109.645,25.997 109.802,25.32 109.76,24.64C109.802,23.97 109.646,23.302 109.31,22.72C109.019,22.288 108.52,22.041 108,22.07Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M117.09,28.59C116.229,28.641 115.395,28.267 114.86,27.59L114.77,27.59C114.83,28.22 114.86,28.59 114.86,28.75L114.86,31.85L113.26,31.85L113.26,20.89L114.55,20.89C114.55,21.03 114.67,21.37 114.78,21.89L114.86,21.89C115.354,21.132 116.217,20.693 117.12,20.74C117.972,20.695 118.794,21.069 119.32,21.74C119.897,22.585 120.175,23.599 120.11,24.62C120.169,25.645 119.888,26.661 119.31,27.51C118.791,28.21 117.961,28.614 117.09,28.59ZM116.7,22.07C116.171,22.022 115.65,22.23 115.3,22.63C114.964,23.163 114.81,23.792 114.86,24.42L114.86,24.66C114.807,25.355 114.96,26.051 115.3,26.66C115.641,27.094 116.18,27.327 116.73,27.28C117.249,27.298 117.74,27.034 118.01,26.59C118.345,25.997 118.502,25.32 118.46,24.64C118.505,23.971 118.352,23.303 118.02,22.72C117.723,22.29 117.222,22.043 116.7,22.07Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
31
.github/assets/badge-windows-store.svg
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 864 312" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g id="Layer_2">
|
||||
<rect x="2.3" y="2.3" width="859.5" height="307.5"/>
|
||||
<path d="M4.5,4.5L859.5,4.5L859.5,307.5L4.5,307.5L4.5,4.5ZM0,312L864,312L864,0L0,0L0,312Z" style="fill:rgb(210,210,210);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<rect x="70.5" y="68.6" width="83.8" height="83.8" style="fill:rgb(242,80,34);"/>
|
||||
<rect x="163" y="68.6" width="83.8" height="83.8" style="fill:rgb(127,186,0);"/>
|
||||
<rect x="70.5" y="161" width="83.8" height="83.8" style="fill:rgb(0,164,239);"/>
|
||||
<rect x="163" y="161" width="83.8" height="83.8" style="fill:rgb(255,185,0);"/>
|
||||
<path d="M408.3,163C408.3,160.5 409.2,158.5 411,156.8C412.8,155.1 414.9,154.3 417.4,154.3C420,154.3 422.2,155.2 423.9,156.9C425.6,158.6 426.5,160.7 426.5,163C426.5,165.4 425.6,167.5 423.8,169.1C422,170.8 419.9,171.6 417.3,171.6C414.7,171.6 412.6,170.8 410.8,169.1C409.2,167.4 408.3,165.4 408.3,163M424.8,244.8L409.9,244.8L409.9,181.3L424.8,181.3L424.8,244.8Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M470.2,233.9C472.4,233.9 474.9,233.4 477.6,232.4C480.3,231.4 482.7,230 485,228.3L485,242.2C482.6,243.6 479.9,244.6 476.9,245.3C473.9,246 470.5,246.4 466.9,246.4C457.6,246.4 450,243.4 444.1,237.5C438.2,231.6 435.3,224 435.3,214.9C435.3,204.7 438.3,196.3 444.3,189.7C450.3,183.1 458.7,179.8 469.7,179.8C472.5,179.8 475.3,180.2 478.2,180.9C481.1,181.6 483.3,182.5 485,183.4L485,197.7C482.7,196 480.3,194.7 477.9,193.8C475.5,192.9 473,192.4 470.5,192.4C464.6,192.4 459.9,194.3 456.2,198.1C452.6,201.9 450.8,207.1 450.8,213.6C450.8,220 452.5,225 456,228.6C459.5,232.2 464.2,233.9 470.2,233.9" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M527.5,180.3C528.7,180.3 529.8,180.4 530.7,180.5C531.6,180.7 532.5,180.9 533.1,181.1L533.1,196.2C532.3,195.6 531.2,195.1 529.7,194.6C528.2,194.1 526.4,193.8 524.2,193.8C520.5,193.8 517.4,195.3 514.9,198.4C512.4,201.5 511.1,206.2 511.1,212.7L511.1,244.8L496.2,244.8L496.2,181.3L511.1,181.3L511.1,191.3L511.3,191.3C512.7,187.8 514.7,185.1 517.5,183.2C520.3,181.3 523.6,180.3 527.5,180.3" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M533.9,214C533.9,203.5 536.9,195.2 542.8,189C548.7,182.9 557,179.8 567.5,179.8C577.4,179.8 585.2,182.8 590.8,188.7C596.4,194.6 599.2,202.6 599.2,212.6C599.2,222.9 596.2,231.1 590.3,237.2C584.4,243.3 576.3,246.3 566.1,246.3C556.3,246.3 548.4,243.4 542.7,237.6C536.8,231.9 533.9,224 533.9,214M549.5,213.5C549.5,220.1 551,225.2 554,228.7C557,232.2 561.3,233.9 566.9,233.9C572.3,233.9 576.5,232.2 579.3,228.7C582.1,225.2 583.6,220 583.6,213.1C583.6,206.3 582.1,201.1 579.2,197.6C576.3,194.1 572.1,192.4 566.8,192.4C561.3,192.4 557.1,194.2 554,197.9C551,201.6 549.5,206.7 549.5,213.5" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M621.4,198C621.4,200.1 622.1,201.8 623.4,203C624.8,204.2 627.8,205.7 632.4,207.6C638.4,210 642.6,212.7 645,215.7C647.4,218.7 648.6,222.3 648.6,226.5C648.6,232.5 646.3,237.3 641.7,240.9C637.1,244.5 630.9,246.3 623.1,246.3C620.5,246.3 617.6,246 614.4,245.3C611.2,244.7 608.5,243.8 606.3,242.9L606.3,228.2C609,230.1 611.9,231.6 615.1,232.7C618.2,233.8 621.1,234.4 623.6,234.4C627,234.4 629.5,233.9 631.1,233C632.7,232.1 633.5,230.5 633.5,228.2C633.5,226.1 632.7,224.4 631,223C629.3,221.6 626.2,219.9 621.5,218.1C616,215.8 612.1,213.2 609.8,210.3C607.5,207.4 606.3,203.7 606.3,199.3C606.3,193.6 608.6,188.9 613.1,185.2C617.6,181.5 623.5,179.7 630.8,179.7C633,179.7 635.5,179.9 638.3,180.4C641.1,180.9 643.4,181.5 645.2,182.3L645.2,196.5C643.2,195.2 640.9,194.1 638.3,193.1C635.7,192.2 633,191.7 630.5,191.7C627.7,191.7 625.4,192.3 623.9,193.4C622.2,194.5 621.4,196.1 621.4,198" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M655.2,214C655.2,203.5 658.2,195.2 664.1,189C670,182.9 678.3,179.8 688.8,179.8C698.7,179.8 706.5,182.8 712.1,188.7C717.7,194.6 720.5,202.6 720.5,212.6C720.5,222.9 717.5,231.1 711.6,237.2C705.7,243.3 697.6,246.3 687.4,246.3C677.6,246.3 669.7,243.4 664,237.6C658.1,231.9 655.2,224 655.2,214M670.7,213.5C670.7,220.1 672.2,225.2 675.2,228.7C678.2,232.2 682.5,233.9 688.1,233.9C693.5,233.9 697.7,232.2 700.5,228.7C703.3,225.2 704.8,220 704.8,213.1C704.8,206.3 703.3,201.1 700.4,197.6C697.5,194.1 693.3,192.4 688,192.4C682.5,192.4 678.3,194.2 675.2,197.9C672.2,201.6 670.7,206.7 670.7,213.5" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M769.8,193.6L769.8,226.2C769.8,233 771.4,238 774.5,241.4C777.6,244.8 782.4,246.4 788.7,246.4C790.8,246.4 793,246.2 795.2,245.7C797.4,245.2 799,244.8 799.9,244.2L799.9,231.8C799,232.4 797.9,232.9 796.7,233.3C795.5,233.7 794.4,233.9 793.6,233.9C790.6,233.9 788.3,233.1 786.9,231.5C785.5,229.9 784.8,227.1 784.8,223.2L784.8,193.5L800,193.5L800,181.3L784.9,181.3L784.9,162.5L769.9,167.1L769.9,181.4L747.6,181.4L747.6,173.7C747.6,169.9 748.4,167 750.1,165C751.8,163 754.2,162.1 757.3,162.1C758.9,162.1 760.3,162.3 761.6,162.7C762.8,163.1 763.7,163.5 764.2,163.8L764.2,150.9C763.1,150.5 761.9,150.3 760.5,150.1C759.1,149.9 757.5,149.8 755.7,149.8C748.9,149.8 743.3,151.9 739,156.2C734.7,160.5 732.5,165.9 732.5,172.6L732.5,181.4L721.9,181.4L721.9,193.6L732.5,193.6L732.5,244.9L747.6,244.9L747.6,193.6L769.8,193.6Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M395.5,156.2L395.5,244.8L380.1,244.8L380.1,175.4L379.9,175.4L352.4,244.8L342.2,244.8L314,175.4L313.8,175.4L313.8,244.8L299.6,244.8L299.6,156.2L321.6,156.2L347.1,221.9L347.5,221.9L374.4,156.2L395.5,156.2Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M340,117.9C334.7,120.9 328.7,122.4 322,122.4C316.9,122.4 312.4,121.3 308.4,119C304.5,116.8 301.4,113.6 299.3,109.5C297.2,105.4 296.1,100.8 296.1,95.7C296.1,90.3 297.3,85.4 299.6,81.2C301.9,77 305.2,73.6 309.5,71.2C313.8,68.8 318.6,67.6 324,67.6C326.7,67.6 329.4,67.9 332.1,68.4C334.8,68.9 337,69.6 338.7,70.4L338.7,78.4C334.7,75.7 329.6,74.4 323.4,74.4C319.7,74.4 316.4,75.3 313.4,77.1C310.4,78.9 308.1,81.4 306.5,84.5C304.9,87.7 304.1,91.3 304.1,95.3C304.1,101.6 305.8,106.6 309.1,110.3C312.4,114 317,115.8 322.8,115.8C326.5,115.8 329.8,115.1 332.6,113.6L332.6,99.9L321.3,99.9L321.3,93.3L340,93.3L340,117.9Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M383.1,104.6L356.9,104.6C357,108.4 358.2,111.3 360.2,113.4C362.3,115.4 365,116.5 368.4,116.5C372.9,116.5 376.8,115.2 380.2,112.6L380.2,119C378.7,120 376.8,120.9 374.5,121.5C372.2,122.1 369.8,122.4 367.4,122.4C361.7,122.4 357.2,120.7 354.1,117.3C351,113.9 349.4,109.1 349.4,102.9C349.4,99.1 350.2,95.6 351.7,92.6C353.2,89.5 355.4,87.1 358.1,85.4C360.8,83.7 363.9,82.8 367.2,82.8C372.1,82.8 376,84.4 378.8,87.6C381.6,90.8 383,95.2 383,100.9L383,104.6L383.1,104.6ZM375.8,98.9C375.8,95.6 375,93.1 373.5,91.4C372,89.7 369.9,88.8 367.1,88.8C364.6,88.8 362.4,89.7 360.5,91.5C358.6,93.3 357.5,95.8 356.9,98.9L375.8,98.9Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M411.5,121.1C410.8,121.5 409.9,121.8 408.6,122C407.4,122.2 406.3,122.3 405.3,122.3C398.3,122.3 394.8,118.4 394.8,110.7L394.8,89.7L388.4,89.7L388.4,83.7L394.8,83.7L394.8,74.7L402.1,72.4L402.1,83.6L411.4,83.6L411.4,89.6L402.1,89.6L402.1,109.4C402.1,111.9 402.5,113.7 403.3,114.7C404.1,115.7 405.5,116.2 407.6,116.2C409,116.2 410.3,115.8 411.4,115L411.4,121.1L411.5,121.1Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M447.7,70.3C447.7,71.5 447.3,72.5 446.4,73.4C445.5,74.3 444.4,74.7 443.1,74.7C441.8,74.7 440.7,74.3 439.8,73.4C438.9,72.6 438.5,71.5 438.5,70.2C438.5,68.9 439,67.8 439.9,67C440.8,66.2 441.9,65.7 443.1,65.7C444.3,65.7 445.4,66.1 446.3,67C447.3,68 447.7,69 447.7,70.3M446.7,121.5L439.4,121.5L439.4,83.7L446.7,83.7L446.7,121.5Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M478.4,121.1C477.7,121.5 476.8,121.8 475.5,122C474.3,122.2 473.2,122.3 472.2,122.3C465.2,122.3 461.7,118.4 461.7,110.7L461.7,89.7L455.3,89.7L455.3,83.7L461.7,83.7L461.7,74.7L469,72.4L469,83.6L478.3,83.6L478.3,89.6L469,89.6L469,109.4C469,111.9 469.4,113.7 470.2,114.7C471,115.7 472.4,116.2 474.5,116.2C475.9,116.2 477.2,115.8 478.3,115L478.3,121.1L478.4,121.1Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M526.2,71.7C525,71.1 523.7,70.7 522.2,70.7C518.1,70.7 516.1,73.2 516.1,78.2L516.1,83.6L524.7,83.6L524.7,89.6L516.1,89.6L516.1,121.4L508.8,121.4L508.8,89.7L502.4,89.7L502.4,83.7L508.8,83.7L508.8,77.9C508.8,73.9 509.9,70.7 512.2,68.3C514.5,65.9 517.6,64.7 521.6,64.7C523.6,64.7 525.1,64.9 526.3,65.4L526.3,71.7L526.2,71.7Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M553.1,91C552.7,90.7 551.9,90.4 550.9,90.1C549.9,89.9 549,89.7 548.3,89.7C545.7,89.7 543.6,90.9 542,93.2C540.4,95.5 539.6,98.5 539.6,102.2L539.6,121.5L532.3,121.5L532.3,83.7L539.6,83.7L539.6,91.3L539.8,91.3C540.6,88.7 541.9,86.7 543.6,85.2C545.3,83.7 547.3,83 549.5,83C551,83 552.2,83.2 553,83.5L553,91L553.1,91Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M594.8,102.4C594.8,108.5 593,113.4 589.5,117C586,120.6 581.3,122.4 575.5,122.4C569.8,122.4 565.2,120.6 561.8,117.1C558.4,113.6 556.7,108.9 556.7,103C556.7,96.7 558.5,91.8 562,88.2C565.5,84.6 570.3,82.8 576.4,82.8C582.1,82.8 586.6,84.5 589.8,88C593.2,91.5 594.8,96.3 594.8,102.4M587.3,102.6C587.3,98.1 586.3,94.7 584.4,92.4C582.4,90.1 579.7,88.9 576.1,88.9C572.5,88.9 569.6,90.1 567.5,92.6C565.4,95 564.4,98.5 564.4,102.9C564.4,107.2 565.4,110.5 567.5,112.9C569.6,115.3 572.4,116.5 576.1,116.5C579.8,116.5 582.6,115.3 584.5,113C586.3,110.5 587.3,107.1 587.3,102.6" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M660.1,121.5L652.8,121.5L652.8,100C652.8,96.1 652.2,93.3 651,91.5C649.8,89.7 647.7,88.9 644.8,88.9C642.4,88.9 640.3,90 638.7,92.2C637,94.4 636.2,97.1 636.2,100.1L636.2,121.5L628.9,121.5L628.9,99.3C628.9,92.4 626.2,88.9 620.9,88.9C618.4,88.9 616.3,90 614.7,92.1C613.1,94.2 612.3,96.9 612.3,100.2L612.3,121.6L605,121.6L605,83.7L612.3,83.7L612.3,89.6L612.4,89.6C615.1,85 619.1,82.8 624.2,82.8C626.6,82.8 628.8,83.5 630.8,84.8C632.7,86.1 634.1,88 635,90.4C636.5,87.8 638.2,85.9 640.3,84.6C642.4,83.4 644.8,82.7 647.6,82.7C655.9,82.7 660,87.8 660,98.1L660,121.5L660.1,121.5Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
45
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/app"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
groups:
|
||||
pub:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/supabase/functions/_cmd"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
groups:
|
||||
docker:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/landing"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
assignees:
|
||||
- "ricoberger"
|
||||
groups:
|
||||
npm:
|
||||
patterns:
|
||||
- "*"
|
||||
235
.github/workflows/continuous-delivery.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -36,13 +36,13 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: ./supabase/functions
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -87,11 +87,14 @@ jobs:
|
||||
|
||||
supabase db push
|
||||
|
||||
supabase functions deploy add-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy add-or-update-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
# supabase functions deploy add-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy delete-user-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy generate-magic-link-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy image-proxy-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy profile-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
# supabase functions deploy profile-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy profile-v2 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy revenuecat-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-create-billing-portal-link-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-create-checkout-session-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
@@ -107,21 +110,26 @@ jobs:
|
||||
|
||||
supabase db push
|
||||
|
||||
supabase functions deploy add-or-update-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy add-source-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy delete-user-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy generate-magic-link-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy image-proxy-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy profile-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy profile-v2 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy revenuecat-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-create-billing-portal-link-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-create-checkout-session-v1 --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-webhooks-v1 --no-verify-jwt --project-ref $PROJECT_ID --import-map supabase/functions/import_map.json
|
||||
|
||||
# The "Web" job builds the Flutter web app and publishes it to Cloudflare Pages. The job only runs when a commit is
|
||||
# pushed to the main branch or a new tag is created.
|
||||
# The "Web" job builds the Flutter web app and publishes it to Cloudflare Pages. The job only runs on pull requests or
|
||||
# when a commit is pushed to the main branch or a new tag is created.
|
||||
#
|
||||
# When the job runs on a pull request it only builds the app but doesn't upload the build to Cloudflare.
|
||||
web:
|
||||
name: Web
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
permissions:
|
||||
contents: read
|
||||
defaults:
|
||||
@@ -130,7 +138,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -162,7 +170,7 @@ jobs:
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.7.12'
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
@@ -179,6 +187,7 @@ jobs:
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
@@ -190,7 +199,7 @@ jobs:
|
||||
# runs for pull requests and when a new release is published.
|
||||
macos:
|
||||
name: macOS
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -200,12 +209,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.7.12'
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
@@ -218,7 +227,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
flutter config --enable-macos-desktop
|
||||
flutter build macos --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}
|
||||
FLUTTER_XCODE_CODE_SIGN_IDENTITY="" FLUTTER_XCODE_CODE_SIGNING_REQUIRED=NO flutter build macos --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
@@ -226,7 +235,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifacts (PR)
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: feeddeck-macos-universal.zip
|
||||
path: app/build/macos/Build/Products/Release/feeddeck-macos-universal.zip
|
||||
@@ -239,10 +248,10 @@ jobs:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: app/build/macos/Build/Products/Release/feeddeck-macos-universal.zip
|
||||
|
||||
# The "Linux" job builds the Flutter Linux app and uploads it to the GitHub release or the pull request. The job only
|
||||
# runs for pull requests and when a new release is published.
|
||||
linux:
|
||||
name: Linux
|
||||
# The "Linux (x86_64)" job builds the Flutter Linux app and uploads it to the GitHub release or the pull request. The
|
||||
# job only runs for pull requests and when a new release is published.
|
||||
linux-x86_64:
|
||||
name: Linux (x86_64)
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
permissions:
|
||||
@@ -253,18 +262,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Packages
|
||||
run: |
|
||||
# Required for Flutter
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||
# Required for Package "media_kit" which is used via "just_audio_media_kit" for Linux and Windows:
|
||||
# See: https://pub.dev/packages/media_kit and https://pub.dev/packages/just_audio_media_kit
|
||||
sudo apt-get install -y libmpv-dev mpv
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.7.12'
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
@@ -281,13 +293,16 @@ jobs:
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
cp linux/flatpak/app.feeddeck.feeddeck.desktop build/linux/x64/release/bundle/
|
||||
cp linux/flatpak/app.feeddeck.feeddeck.metainfo.xml build/linux/x64/release/bundle/
|
||||
cp linux/flatpak/app.feeddeck.feeddeck.svg build/linux/x64/release/bundle/
|
||||
cd build
|
||||
cp -r linux/x64/release/bundle/ feeddeck-linux-x86_64
|
||||
tar -czf feeddeck-linux-x86_64.tar.gz feeddeck-linux-x86_64
|
||||
|
||||
- name: Upload Artifacts (PR)
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: feeddeck-linux-x86_64.tar.gz
|
||||
path: app/build/feeddeck-linux-x86_64.tar.gz
|
||||
@@ -300,11 +315,82 @@ jobs:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: app/build/feeddeck-linux-x86_64.tar.gz
|
||||
|
||||
# The "Linux (arm64)" job builds the Flutter Linux app and uploads it to the GitHub release or the pull request. The
|
||||
# job only runs for pull requests and when a new release is published.
|
||||
#
|
||||
# NOTE: Normally this job should run for every pull request and when a new release is published, but since we have to
|
||||
# pay for the "ubicloud-standard-2-arm" runner, we only run the job when a new release is published.
|
||||
linux-arm64:
|
||||
name: Linux (arm64)
|
||||
runs-on: ubicloud-standard-2-arm
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
# if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
permissions:
|
||||
contents: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "app"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Packages
|
||||
run: |
|
||||
# Required for Flutter
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||
# Required for Package "media_kit" which is used via "just_audio_media_kit" for Linux and Windows:
|
||||
# See: https://pub.dev/packages/media_kit and https://pub.dev/packages/just_audio_media_kit
|
||||
sudo apt-get install -y libmpv-dev mpv
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'master'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
flutter pub get
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
flutter config --enable-linux-desktop
|
||||
flutter build linux --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
cp linux/flatpak/app.feeddeck.feeddeck.desktop build/linux/arm64/release/bundle/
|
||||
cp linux/flatpak/app.feeddeck.feeddeck.metainfo.xml build/linux/arm64/release/bundle/
|
||||
cp linux/flatpak/app.feeddeck.feeddeck.svg build/linux/arm64/release/bundle/
|
||||
cd build
|
||||
cp -r linux/arm64/release/bundle/ feeddeck-linux-arm64
|
||||
tar -czf feeddeck-linux-arm64.tar.gz feeddeck-linux-arm64
|
||||
|
||||
- name: Upload Artifacts (PR)
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: feeddeck-linux-arm64.tar.gz
|
||||
path: app/build/feeddeck-linux-arm64.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Artifacts (Release)
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: app/build/feeddeck-linux-arm64.tar.gz
|
||||
|
||||
# The "Windows" job builds the Flutter Windows app and uploads it to the GitHub release or the pull request. The job
|
||||
# only runs for pull requests and when a new release is published.
|
||||
windows:
|
||||
name: Windows
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2019
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -314,12 +400,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.7.12'
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
@@ -333,19 +419,22 @@ jobs:
|
||||
run: |
|
||||
flutter config --enable-windows-desktop
|
||||
flutter build windows --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}
|
||||
- name: Create Archive
|
||||
run: |
|
||||
Compress-Archive -Path build/windows/x64/runner/Release/* -Destination feeddeck-windows-x86_64.zip
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
flutter pub run msix:create --output-path build --output-name feeddeck
|
||||
cd build
|
||||
7z a -tzip feeddeck-windows-x86_64.zip feeddeck.msix
|
||||
7z a -tzip feeddeck-windows-x86_64-msix.zip feeddeck.msix
|
||||
|
||||
- name: Upload Artifacts (PR)
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: feeddeck-windows-x86_64.zip
|
||||
path: app/build/feeddeck-windows-x86_64.zip
|
||||
path: app/feeddeck-windows-x86_64.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Artifacts (Release)
|
||||
@@ -353,4 +442,86 @@ jobs:
|
||||
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: app/build/feeddeck-windows-x86_64.zip
|
||||
asset_path: app/feeddeck-windows-x86_64.zip
|
||||
|
||||
- name: Upload Artifacts (PR)
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: feeddeck-windows-x86_64-msix.zip
|
||||
path: app/build/feeddeck-windows-x86_64-msix.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Artifacts (Release)
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: app/build/feeddeck-windows-x86_64-msix.zip
|
||||
|
||||
# The "iOS" job builds the Flutter iOS app on every pull request. This is only used to test that the build of the iOS
|
||||
# app works. The artifact of the build isn't uploaded / used.
|
||||
ios:
|
||||
name: iOS
|
||||
runs-on: macos-14
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "app"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
flutter pub get
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
flutter config --enable-ios
|
||||
flutter build ipa --no-codesign --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}
|
||||
|
||||
# The "Android" job builds the Flutter Android app on every pull request. This is only used to test that the build of
|
||||
# the Android app works. The artifact of the build isn't uploaded / used.
|
||||
android:
|
||||
name: Android
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' || (github.event_name == 'release' && github.event.action == 'published')
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "app"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
flutter pub get
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
flutter config --enable-android
|
||||
flutter build appbundle --release --dart-define SUPABASE_URL=${{ secrets.SUPABASE_PROD_URL }} --dart-define SUPABASE_ANON_KEY=${{ secrets.SUPABASE_PROD_ANON_KEY }} --dart-define SUPABASE_SITE_URL=${{ secrets.SUPABASE_PROD_SITE_URL }} --dart-define GOOGLE_CLIENT_ID=${{ secrets.SUPABASE_PROD_GOOGLE_CLIENT_ID }}
|
||||
|
||||
53
.github/workflows/continuous-integration.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
flutter:
|
||||
name: Flutter
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: "app"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.22.2'
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
|
||||
cache-path: '${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
flutter pub get
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
flutter test
|
||||
|
||||
deno:
|
||||
name: Deno
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
deno test --allow-env --import-map=supabase/functions/import_map.json supabase/functions
|
||||
14
.github/workflows/release.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Update Changelog
|
||||
uses: release-drafter/release-drafter@v5
|
||||
uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
config-name: release.yaml
|
||||
disable-autolabeler: true
|
||||
@@ -37,17 +37,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "16"
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
cache-dependency-path: landing/package-lock.json
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
npm run build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./landing/out
|
||||
|
||||
@@ -78,4 +78,4 @@ jobs:
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
7
.gitignore
vendored
@@ -1,8 +1,11 @@
|
||||
# Visual Studio Code Launch Configurations
|
||||
.vscode/launch.json
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
|
||||
# Environment Variables
|
||||
/supabase/.env.local
|
||||
/supabase/.env.dev
|
||||
/supabase/.env.stage
|
||||
/supabase/.env.prod
|
||||
|
||||
# Deno
|
||||
/coverage_deno
|
||||
|
||||
17
.vscode/settings.json
vendored
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.unstable": true,
|
||||
"deno.lint": true,
|
||||
"deno.enablePaths": [
|
||||
"./supabase/functions"
|
||||
],
|
||||
"deno.importMap": "./supabase/functions/import_map.json",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
}
|
||||
}
|
||||
195
CONTRIBUTING.md
@@ -57,116 +57,46 @@ check your installed version:
|
||||
```sh
|
||||
$ flutter --version
|
||||
|
||||
Flutter 3.7.12 • channel stable • https://github.com/flutter/flutter.git
|
||||
Framework • revision 4d9e56e694 (8 weeks ago) • 2023-04-17 21:47:46 -0400
|
||||
Engine • revision 1a65d409c7
|
||||
Tools • Dart 2.19.6 • DevTools 2.20.1
|
||||
|
||||
Flutter 3.22.2 • channel stable • https://github.com/flutter/flutter.git
|
||||
Framework • revision 761747bfc5 (3 days ago) • 2024-06-05 22:15:13 +0200
|
||||
Engine • revision edd8546116
|
||||
Tools • Dart 3.4.3 • DevTools 2.34.3
|
||||
|
||||
$ deno --version
|
||||
deno 1.36.4 (release, aarch64-apple-darwin)
|
||||
v8 11.6.189.12
|
||||
typescript 5.1.6
|
||||
|
||||
deno 1.40.2 (release, aarch64-apple-darwin)
|
||||
v8 12.1.285.6
|
||||
typescript 5.3.3
|
||||
```
|
||||
|
||||
### Working with Flutter
|
||||
|
||||
We are recommending to use the
|
||||
[Visual Studio Code](https://docs.flutter.dev/development/tools/vs-code)
|
||||
extensions for development.
|
||||
To run the app you can use the [`run.sh`](./app/run.sh) script, which will
|
||||
automatically load the `.env` file from the Supabase project and passes the
|
||||
required variables to the `flutter run` command:
|
||||
|
||||
The easiest way to run the Flutter app within Visual Studio Code is to create a
|
||||
`.vscode/launch.json` file. Within the different configurations you have to
|
||||
provide the following arguments: `--dart-define SUPABASE_URL=<SUPABASE_URL>`,
|
||||
`--dart-define SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>`,
|
||||
`--dart-define SUPABASE_SITE_URL=<SUPABASE_SITE_URL>` and
|
||||
`--dart-define GOOGLE_CLIENT_ID=<GOOGLE_CLIENT_ID>`.
|
||||
|
||||
<details>
|
||||
<summary>Example: `.vscode/launch.json`</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "FeedDeck (Chrome)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"--web-browser-flag=--disable-web-security",
|
||||
"--dart-define",
|
||||
"SUPABASE_URL=<SUPABASE_URL>",
|
||||
"--dart-define",
|
||||
"SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>",
|
||||
"--dart-define",
|
||||
"SUPABASE_SITE_URL=<SUPABASE_SITE_URL>",
|
||||
"--dart-define",
|
||||
"GOOGLE_CLIENT_ID=<GOOGLE_CLIENT_ID>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FeedDeck (Web Server)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"web-server",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"--dart-define",
|
||||
"SUPABASE_URL=<SUPABASE_URL>",
|
||||
"--dart-define",
|
||||
"SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>",
|
||||
"--dart-define",
|
||||
"SUPABASE_SITE_URL=<SUPABASE_SITE_URL>",
|
||||
"--dart-define",
|
||||
"GOOGLE_CLIENT_ID=<GOOGLE_CLIENT_ID>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FeedDeck (iOS)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"iPhone 14 Pro Max",
|
||||
"--dart-define",
|
||||
"SUPABASE_URL=<SUPABASE_URL>",
|
||||
"--dart-define",
|
||||
"SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>",
|
||||
"--dart-define",
|
||||
"SUPABASE_SITE_URL=<SUPABASE_SITE_URL>",
|
||||
"--dart-define",
|
||||
"GOOGLE_CLIENT_ID=<GOOGLE_CLIENT_ID>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FeedDeck (macOS)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"macOS",
|
||||
"--dart-define",
|
||||
"SUPABASE_URL=<SUPABASE_URL>",
|
||||
"--dart-define",
|
||||
"SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>",
|
||||
"--dart-define",
|
||||
"SUPABASE_SITE_URL=<SUPABASE_SITE_URL>",
|
||||
"--dart-define",
|
||||
"GOOGLE_CLIENT_ID=<GOOGLE_CLIENT_ID>"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```sh
|
||||
./run.sh --device="chrome" --environment="local"
|
||||
```
|
||||
|
||||
</details>
|
||||
To run the tests the following command can be used:
|
||||
|
||||
```sh
|
||||
flutter test
|
||||
```
|
||||
|
||||
To check the test coverage the `--coverage` flag can be added to the command and
|
||||
an HTML report can be generated:
|
||||
|
||||
```sh
|
||||
flutter test --coverage
|
||||
|
||||
# To generate the HTML report lcov is required, which can be installed via Homebrew:
|
||||
brew install lcov
|
||||
|
||||
genhtml coverage/lcov.info -o coverage/html
|
||||
open coverage/html/index.html
|
||||
```
|
||||
|
||||
#### Sort all Imports
|
||||
|
||||
@@ -300,6 +230,38 @@ cd supabase/functions/_cmd
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
To build the Docker image, the following commands can be run:
|
||||
|
||||
```sh
|
||||
docker build -f supabase/functions/_cmd/Dockerfile -t ghcr.io/feeddeck/feeddeck:dev supabase/functions
|
||||
|
||||
# To build the Docker image for another platform use the following:
|
||||
docker buildx build --platform linux/amd64 -f supabase/functions/_cmd/Dockerfile -t ghcr.io/feeddeck/feeddeck:dev supabase/functions
|
||||
|
||||
# The Docker image can then be used to run the scheduler, worker or tools, e.g.
|
||||
docker run ghcr.io/feeddeck/feeddeck:dev tools get-feed '{"type": "reddit", "options": {"reddit": "/r/kubernetes"}}'
|
||||
```
|
||||
|
||||
To run the tests for our code, the following command can be used:
|
||||
|
||||
```sh
|
||||
deno test --allow-env --import-map=supabase/functions/import_map.json supabase/functions
|
||||
```
|
||||
|
||||
To check the test coverage the `--coverage` flag can be added to the command and
|
||||
an HTML report can be generated:
|
||||
|
||||
```sh
|
||||
deno test --allow-env --import-map=supabase/functions/import_map.json supabase/functions --coverage=coverage_deno
|
||||
|
||||
# To generate the HTML report lcov is required, which can be installed via Homebrew:
|
||||
brew install lcov
|
||||
|
||||
deno coverage coverage_deno --lcov --output=coverage_deno/coverage_deno.lcov
|
||||
genhtml -o coverage_deno/html coverage_deno/coverage_deno.lcov
|
||||
open coverage_deno/html/index.html
|
||||
```
|
||||
|
||||
## Hosting
|
||||
|
||||
FeedDeck uses Supabase as backend. For Supabase we can use
|
||||
@@ -321,11 +283,14 @@ supabase secrets set --env-file supabase/.env
|
||||
supabase secrets list
|
||||
|
||||
# Deploy all functions
|
||||
supabase functions deploy add-or-update-source-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy add-source-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy delete-user-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy generate-magic-link-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy image-proxy-v1 --no-verify-jwt --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy profile-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy profile-v2 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy revenuecat-webhooks-v1 --no-verify-jwt --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-create-billing-portal-link-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-create-checkout-session-v1 --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
supabase functions deploy stripe-webhooks-v1 --no-verify-jwt --project-ref <PROJECT-ID> --import-map supabase/functions/import_map.json
|
||||
@@ -411,26 +376,32 @@ Android, macOS, Windows and Linux if you do not want to use the official ones.
|
||||
2. Update the `version` key and the `msix_config.msix_version` key in the
|
||||
`pubspec.yaml` file.
|
||||
|
||||
3. Delete the `build/` and `.dart_tool/` directories via the `flutter clean`
|
||||
3. Add the new release to the `releases` section in
|
||||
[`app.feeddeck.feeddeck.metainfo.xml`](app/linux/flatpak/app.feeddeck.feeddeck.metainfo.xml).
|
||||
|
||||
4. Delete the `build/` and `.dart_tool/` directories via the `flutter clean`
|
||||
command.
|
||||
|
||||
4. Build the app for Web by running `flutter build web`. The build can be found
|
||||
5. Build the app for Web by running `flutter build web`. The build can be found
|
||||
at `app/build/web` and must be uploaded to your hosting provider.
|
||||
|
||||
5. Build the app for Linux by running `flutter build linux --release`.
|
||||
6. Build the app for Linux by running `flutter build linux --release`. Update
|
||||
the `app.feeddeck.feeddeck.yml` file at
|
||||
[github.com/flathub/app.feeddeck.feeddeck](https://github.com/flathub/app.feeddeck.feeddeck)
|
||||
with the new release.
|
||||
|
||||
6. Build the app for macOS by running `flutter build macos --release`. Open
|
||||
7. Build the app for macOS by running `flutter build macos --release`. Open
|
||||
Xcode and select **Product** > **Archive** to create and open the archive.
|
||||
After that the **Validate App** and **Distribute App** options can be used to
|
||||
upload the build to
|
||||
[https://appstoreconnect.apple.com](https://appstoreconnect.apple.com).
|
||||
|
||||
7. Build the app for Windows by running `flutter build windows --release`. and
|
||||
8. Build the app for Windows by running `flutter build windows --release`. and
|
||||
`flutter pub run msix:create --output-path build --output-name feeddeck`. The
|
||||
build can be found at `app/build/feeddeck.msix` and must be uploaded to
|
||||
[https://partner.microsoft.com/en-us/dashboard/products/9NPHPGRRCT5H/overview](https://partner.microsoft.com/en-us/dashboard/products/9NPHPGRRCT5H/overview).
|
||||
|
||||
8. Create a file `app/android/key.properties` with the following content:
|
||||
9. Create a file `app/android/key.properties` with the following content:
|
||||
|
||||
```plain
|
||||
storePassword=
|
||||
@@ -439,12 +410,12 @@ Android, macOS, Windows and Linux if you do not want to use the official ones.
|
||||
storeFile=
|
||||
```
|
||||
|
||||
9. Build the app for Android by running `flutter build appbundle`. The build can
|
||||
be found at `app/build/app/outputs/bundle/release/app-release.aab` and must
|
||||
be uploaded to
|
||||
[https://play.google.com/apps/publish](https://play.google.com/apps/publish).
|
||||
10. Build the app for Android by running `flutter build appbundle`. The build
|
||||
can be found at `app/build/app/outputs/bundle/release/app-release.aab` and
|
||||
must be uploaded to
|
||||
[https://play.google.com/apps/publish](https://play.google.com/apps/publish).
|
||||
|
||||
10. Build the app for iOS by running `flutter build ipa`. The build can be found
|
||||
11. Build the app for iOS by running `flutter build ipa`. The build can be found
|
||||
at `app/build/ios/archive/Runner.xcarchive` and must be opened in Xcode. In
|
||||
Xcode the **Validate App** and **Distribute App** options can be used to
|
||||
upload the build to
|
||||
|
||||
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Rico Berger
|
||||
Copyright (c) 2024 Rico Berger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
13
README.md
@@ -7,9 +7,12 @@ TweetDeck. FeedDeck allows you to follow your favorite feeds in one place on all
|
||||
platforms.
|
||||
|
||||
<p>
|
||||
<a href="https://feeddeck.app/download" target="_blank"><img src=".github/assets/badge-app-store.png" height="50"></a>
|
||||
<a href="https://feeddeck.app/download" target="_blank"><img src=".github/assets/badge-google-play.png" height="50"></a>
|
||||
<a href="https://feeddeck.app/download" target="_self"><img src=".github/assets/badge-desktop.png" height="50"></a>
|
||||
<a href="https://apps.apple.com/us/app/feeddeck/id6451055362" target="_blank"><img src=".github/assets/badge-app-store.svg" height="50"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=app.feeddeck.feeddeck" target="_blank"><img src=".github/assets/badge-google-play.svg" height="50"></a>
|
||||
<a href="https://app.feeddeck.app" target="_blank"><img src=".github/assets/badge-web.svg" height="50"></a>
|
||||
<a href="https://apps.apple.com/us/app/feeddeck/id6451055362" target="_blank"><img src=".github/assets/badge-mac-app-store.svg" height="50"></a>
|
||||
<a href="https://www.microsoft.com/store/apps/9NPHPGRRCT5H" target="_blank"><img src=".github/assets/badge-windows-store.svg" height="50"></a>
|
||||
<a href="https://flathub.org/en/apps/app.feeddeck.feeddeck" target="_blank"><img src=".github/assets/badge-flathub.svg" height="50"></a>
|
||||
</p>
|
||||
|
||||
<img src=".github/assets/screenshot.png" width="100%" />
|
||||
@@ -28,8 +31,8 @@ platforms. FeedDeck is written in [Flutter](https://flutter.dev/) and uses
|
||||
- **RSS and Social Media Feeds:** Follow your favorite RSS and social media
|
||||
feeds.
|
||||
- **News:** Get the latest news from your favorite RSS feeds and Google News.
|
||||
- **Social Media:** Follow your friends and favorite topics on Medium, Nitter,
|
||||
Reddit, Tumblr and X.
|
||||
- **Social Media:** Follow your friends and favorite topics on Medium, Reddit
|
||||
and Tumblr.
|
||||
- **GitHub:** Get your GitHub notifications and follow your repository
|
||||
activities.
|
||||
- **Podcasts:** Follow and listen to your favorite podcasts, via the built-in
|
||||
|
||||
1
app/.gitignore
vendored
@@ -31,6 +31,7 @@ migrate_working_dir/
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
@@ -12,11 +18,6 @@ if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
@@ -27,21 +28,18 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
namespace "app.feeddeck.feeddeck"
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -49,14 +47,13 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "app.feeddeck.feeddeck"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
// minSdkVersion flutter.minSdkVersion
|
||||
// targetSdkVersion flutter.targetSdkVersion
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@@ -72,7 +69,12 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
// For testing purposes we sign with dummy credentials if no key properties are given.
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,6 +84,5 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.google.android.gms:play-services-auth:20.6.0'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.feeddeck.feeddeck">
|
||||
<application
|
||||
android:label="feeddeck"
|
||||
android:label="FeedDeck"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true"
|
||||
|
||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
@@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
@@ -26,6 +13,6 @@ subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}
|
||||
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.1.2" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
1
app/devtools_options.yaml
Normal file
@@ -0,0 +1 @@
|
||||
extensions:
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>11.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '11.0'
|
||||
platform :ios, '12.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@@ -8,27 +8,40 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- just_audio (0.0.1):
|
||||
- Flutter
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- purchases_flutter (6.30.2):
|
||||
- Flutter
|
||||
- PurchasesHybridCommon (= 11.1.0)
|
||||
- PurchasesHybridCommon (11.1.0):
|
||||
- RevenueCat (= 4.43.2)
|
||||
- RevenueCat (4.43.2)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sign_in_with_apple (0.0.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.2):
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -38,17 +51,24 @@ DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- just_audio (from `.symlinks/plugins/just_audio/ios`)
|
||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- purchases_flutter (from `.symlinks/plugins/purchases_flutter/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- FMDB
|
||||
- PurchasesHybridCommon
|
||||
- RevenueCat
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
@@ -63,37 +83,56 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
just_audio:
|
||||
:path: ".symlinks/plugins/just_audio/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
|
||||
media_kit_video:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
purchases_flutter:
|
||||
:path: ".symlinks/plugins/purchases_flutter/ios"
|
||||
screen_brightness_ios:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/ios"
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sign_in_with_apple:
|
||||
:path: ".symlinks/plugins/sign_in_with_apple/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
volume_controller:
|
||||
:path: ".symlinks/plugins/volume_controller/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
||||
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
|
||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
|
||||
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
|
||||
path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
|
||||
shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
purchases_flutter: 42d5544e7730ea89a88cc2f008b7c700fd147052
|
||||
PurchasesHybridCommon: 4022d5944cb30ec44ba5159e42aa161fe0e30175
|
||||
RevenueCat: 3d934653b7e8b09af88fd47e9e84cfaf5d0a89ba
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||
|
||||
PODFILE CHECKSUM: 92152382e3e2d8bbf76cfce953216c7c829d94ce
|
||||
PODFILE CHECKSUM: 016564c560c4c9dbcb210e12c7aa6039072645f1
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
11A5CB70E48EE53811F041B1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4F63A76E5CF724D941DA24D /* Pods_Runner.framework */; };
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
55F35B592ABF74D1007331B3 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55F35B582ABF74D1007331B3 /* StoreKit.framework */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
@@ -34,6 +35,7 @@
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
551175C42A39020E00A80299 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
55F35B582ABF74D1007331B3 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
5C1B3914BEEA60B960FC050D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -56,6 +58,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
11A5CB70E48EE53811F041B1 /* Pods_Runner.framework in Frameworks */,
|
||||
55F35B592ABF74D1007331B3 /* StoreKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -121,6 +124,7 @@
|
||||
AC99711BA86FAD05300ADB7E /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
55F35B582ABF74D1007331B3 /* StoreKit.framework */,
|
||||
E4F63A76E5CF724D941DA24D /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
@@ -157,7 +161,7 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
@@ -228,6 +232,7 @@
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
@@ -343,7 +348,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -423,7 +428,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -472,7 +477,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -20,14 +22,38 @@
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>app.feeddeck.feeddeck</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
@@ -43,31 +69,5 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<true/>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>app.feeddeck.feeddeck</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||
import 'package:just_audio_background/just_audio_background.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'package:feeddeck/repositories/app_repository.dart';
|
||||
import 'package:feeddeck/repositories/layout_repository.dart';
|
||||
import 'package:feeddeck/repositories/profile_repository.dart';
|
||||
import 'package:feeddeck/repositories/settings_repository.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/confirmation/confirmation.dart';
|
||||
import 'package:feeddeck/widgets/home/home.dart';
|
||||
import 'package:feeddeck/widgets/reset_password/reset_password.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_audio_palyer/item_audio_player_init/item_audio_player_init.dart';
|
||||
|
||||
/// Before we are calling [runApp] we have to ensure that the widget bindings
|
||||
/// are initialized, so that we can preserve the splash screen until we are done
|
||||
@@ -44,13 +48,26 @@ void main() async {
|
||||
});
|
||||
}
|
||||
|
||||
/// Initialize the [media_kit] packages, so that we can play audio and video
|
||||
/// files.
|
||||
MediaKit.ensureInitialized();
|
||||
if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
|
||||
ItemAudioPlayerInit().init();
|
||||
}
|
||||
|
||||
/// Initialize the [just_audio_background] package, so that we can play audio
|
||||
/// files in the background.
|
||||
await JustAudioBackground.init(
|
||||
androidNotificationChannelId: 'com.ryanheise.bg_demo.channel.audio',
|
||||
androidNotificationChannelName: 'Audio playback',
|
||||
androidNotificationOngoing: true,
|
||||
);
|
||||
///
|
||||
/// We can not initialize the [just_audio_background] package on Windows and
|
||||
/// Linux, because then the returned duration in the `_player.durationStream`
|
||||
/// isn't working correctly in the [ItemAudioPlayer] widget.
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
|
||||
await JustAudioBackground.init(
|
||||
androidNotificationChannelId: 'com.ryanheise.bg_demo.channel.audio',
|
||||
androidNotificationChannelName: 'Audio playback',
|
||||
androidNotificationOngoing: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// For the ewb we have to use the path url strategy, so that the redirect
|
||||
/// within Supabase is working in all cases. On all other platforms this is a
|
||||
@@ -65,6 +82,23 @@ void main() async {
|
||||
runApp(const FeedDeckApp());
|
||||
}
|
||||
|
||||
/// [FeedDeckScrollBehavior] changes the scrolling behavior of the app. This is
|
||||
/// required to enable scrolling on desktop via drag, which is the only way that
|
||||
/// a user can scroll vertically with a mouse via drag.
|
||||
///
|
||||
/// E.g. this is required for the [ColumnLayoutSources] widget, where we rely on
|
||||
/// the drag gesture to scroll the list of sources.
|
||||
class FeedDeckScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices => {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.unknown,
|
||||
};
|
||||
}
|
||||
|
||||
/// [onGenerateRoute] is used in `onGenerateRoute` and `onGenerateInitialRoutes`
|
||||
/// of the [MaterialApp] to add handling for some special routes were do not
|
||||
/// want to render the [Home] widget (e.g. set a new password).
|
||||
@@ -106,6 +140,7 @@ class FeedDeckApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => LayoutRepository()),
|
||||
ChangeNotifierProvider(create: (_) => AppRepository()),
|
||||
ChangeNotifierProvider(create: (_) => ProfileRepository()),
|
||||
],
|
||||
@@ -131,6 +166,7 @@ class FeedDeckApp extends StatelessWidget {
|
||||
),
|
||||
canvasColor: Constants.canvasColor,
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: true,
|
||||
backgroundColor: Constants.appBarBackgroundColor,
|
||||
scrolledUnderElevation: Constants.scrolledUnderElevation,
|
||||
elevation: Constants.appBarElevation,
|
||||
@@ -173,6 +209,7 @@ class FeedDeckApp extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
scrollBehavior: FeedDeckScrollBehavior(),
|
||||
onGenerateInitialRoutes: (initialRoute) =>
|
||||
[onGenerateRoute(RouteSettings(name: initialRoute))],
|
||||
onGenerateRoute: (RouteSettings settings) =>
|
||||
|
||||
@@ -29,6 +29,40 @@ FDProfileTier getProfileTierFromString(String state) {
|
||||
return FDProfileTier.free;
|
||||
}
|
||||
|
||||
/// [FDProfileSubscriptionProvider] is a enum value which defines the different
|
||||
/// subscription providers for a profile. A user can use [stripe] or
|
||||
/// [revenuecat] to get a premium account.
|
||||
enum FDProfileSubscriptionProvider {
|
||||
stripe,
|
||||
revenuecat,
|
||||
}
|
||||
|
||||
/// [FDProfileSubscriptionProviderExtension] defines all extensions which are
|
||||
/// available for the [FDProfileSubscriptionProvider] enum type.
|
||||
extension FDProfileSubscriptionProviderExtension
|
||||
on FDProfileSubscriptionProvider {
|
||||
/// [toShortString] returns a short string of the source type which can safely
|
||||
/// be passed to our database.
|
||||
String toShortString() {
|
||||
return toString().split('.').last;
|
||||
}
|
||||
}
|
||||
|
||||
/// [getSourceTypeFromString] returns the [FDProfileSubscriptionProvider] from
|
||||
/// his string representation. This is used to parse the JSON value returned by
|
||||
/// our database into correct enum value in the [FDSource] model.
|
||||
FDProfileSubscriptionProvider? getProfileSubscriptionProviderFromString(
|
||||
String state,
|
||||
) {
|
||||
for (FDProfileSubscriptionProvider element
|
||||
in FDProfileSubscriptionProvider.values) {
|
||||
if (element.toShortString() == state) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [FDProfile] is the model for a profile of a user in our app. The following
|
||||
/// fields are required for a profile:
|
||||
/// - An [id] to uniquely identify a column in the database
|
||||
@@ -38,6 +72,7 @@ FDProfileTier getProfileTierFromString(String state) {
|
||||
class FDProfile {
|
||||
String id;
|
||||
FDProfileTier tier;
|
||||
FDProfileSubscriptionProvider? subscriptionProvider;
|
||||
bool accountGithub;
|
||||
int createdAt;
|
||||
int updatedAt;
|
||||
@@ -45,6 +80,7 @@ class FDProfile {
|
||||
FDProfile({
|
||||
required this.id,
|
||||
required this.tier,
|
||||
required this.subscriptionProvider,
|
||||
required this.accountGithub,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@@ -54,6 +90,12 @@ class FDProfile {
|
||||
return FDProfile(
|
||||
id: data['id'],
|
||||
tier: getProfileTierFromString(data['tier']),
|
||||
subscriptionProvider: data.containsKey('subscriptionProvider') &&
|
||||
data['subscriptionProvider'] != null
|
||||
? getProfileSubscriptionProviderFromString(
|
||||
data['subscriptionProvider'],
|
||||
)
|
||||
: null,
|
||||
accountGithub: data['accountGithub'],
|
||||
createdAt: data['createdAt'],
|
||||
updatedAt: data['updatedAt'],
|
||||
|
||||
@@ -4,20 +4,23 @@ import 'package:feeddeck/models/sources/github.dart';
|
||||
import 'package:feeddeck/models/sources/googlenews.dart';
|
||||
import 'package:feeddeck/models/sources/stackoverflow.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/utils/fd_icons.dart';
|
||||
|
||||
/// [FDSourceType] is a enum value which defines the source type. A source can
|
||||
/// have one of the following types:
|
||||
/// - [fourchan]
|
||||
/// - [github]
|
||||
/// - [googlenews]
|
||||
/// - [lemmy]
|
||||
/// - [mastodon]
|
||||
/// - [medium]
|
||||
/// - [nitter]
|
||||
/// - [pinterest]
|
||||
/// - [podcast]
|
||||
/// - [reddit]
|
||||
/// - [rss]
|
||||
/// - [stackoverflow]
|
||||
/// - [tumblr]
|
||||
/// - [x]
|
||||
/// - [youtube]
|
||||
///
|
||||
/// The [none] value is not valid and just here as a fallback in case sth. odd
|
||||
@@ -25,17 +28,19 @@ import 'package:feeddeck/utils/constants.dart';
|
||||
/// the list, so that we can loop though the types in a ListView / GridView
|
||||
/// builder via `FDSourceType.values.length - 1`.
|
||||
enum FDSourceType {
|
||||
fourchan,
|
||||
github,
|
||||
googlenews,
|
||||
lemmy,
|
||||
mastodon,
|
||||
medium,
|
||||
nitter,
|
||||
pinterest,
|
||||
podcast,
|
||||
reddit,
|
||||
rss,
|
||||
stackoverflow,
|
||||
tumblr,
|
||||
// x,
|
||||
youtube,
|
||||
none,
|
||||
}
|
||||
@@ -52,16 +57,22 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
/// [toLocalizedString] returns a localized string for a source type.
|
||||
String toLocalizedString() {
|
||||
switch (this) {
|
||||
case FDSourceType.fourchan:
|
||||
return '4chan';
|
||||
case FDSourceType.github:
|
||||
return 'GitHub';
|
||||
case FDSourceType.googlenews:
|
||||
return 'Google News';
|
||||
case FDSourceType.lemmy:
|
||||
return 'Lemmy';
|
||||
case FDSourceType.mastodon:
|
||||
return 'Mastodon';
|
||||
case FDSourceType.medium:
|
||||
return 'Medium';
|
||||
case FDSourceType.nitter:
|
||||
return 'Nitter';
|
||||
case FDSourceType.pinterest:
|
||||
return 'Pinterest';
|
||||
case FDSourceType.podcast:
|
||||
return 'Podcast';
|
||||
case FDSourceType.reddit:
|
||||
@@ -72,8 +83,6 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
return 'StackOverflow';
|
||||
case FDSourceType.tumblr:
|
||||
return 'Tumblr';
|
||||
// case FDSourceType.x:
|
||||
// return 'X';
|
||||
case FDSourceType.youtube:
|
||||
return 'YouTube';
|
||||
default:
|
||||
@@ -81,20 +90,61 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
}
|
||||
}
|
||||
|
||||
/// [color] returns the brand color for a source type, which can be used as
|
||||
/// background color for the icon of a source type.
|
||||
Color get color {
|
||||
/// [icon] returns the icon for a source.
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case FDSourceType.fourchan:
|
||||
return FDIcons.fourchan;
|
||||
case FDSourceType.github:
|
||||
return FDIcons.github;
|
||||
case FDSourceType.googlenews:
|
||||
return FDIcons.googlenews;
|
||||
case FDSourceType.lemmy:
|
||||
return FDIcons.lemmy;
|
||||
case FDSourceType.mastodon:
|
||||
return FDIcons.mastodon;
|
||||
case FDSourceType.medium:
|
||||
return FDIcons.medium;
|
||||
case FDSourceType.nitter:
|
||||
return FDIcons.nitter;
|
||||
case FDSourceType.pinterest:
|
||||
return FDIcons.pinterest;
|
||||
case FDSourceType.podcast:
|
||||
return Icons.podcasts;
|
||||
case FDSourceType.reddit:
|
||||
return FDIcons.reddit;
|
||||
case FDSourceType.rss:
|
||||
return FDIcons.rss;
|
||||
case FDSourceType.stackoverflow:
|
||||
return FDIcons.stackoverflow;
|
||||
case FDSourceType.tumblr:
|
||||
return FDIcons.tumblr;
|
||||
case FDSourceType.youtube:
|
||||
return FDIcons.youtube;
|
||||
default:
|
||||
return FDIcons.feeddeck;
|
||||
}
|
||||
}
|
||||
|
||||
/// [bgColor] returns the background color for the source icon.
|
||||
Color get bgColor {
|
||||
switch (this) {
|
||||
case FDSourceType.fourchan:
|
||||
return const Color(0xff880000);
|
||||
case FDSourceType.github:
|
||||
return const Color(0xff000000);
|
||||
case FDSourceType.googlenews:
|
||||
return const Color(0xff4285f4);
|
||||
case FDSourceType.lemmy:
|
||||
return const Color(0xff00bc8c);
|
||||
case FDSourceType.mastodon:
|
||||
return const Color(0xff6364ff);
|
||||
case FDSourceType.medium:
|
||||
return const Color(0xff000000);
|
||||
case FDSourceType.nitter:
|
||||
return const Color(0xffff6c60);
|
||||
case FDSourceType.pinterest:
|
||||
return const Color(0xffe60023);
|
||||
case FDSourceType.podcast:
|
||||
return const Color(0xff872ec4);
|
||||
case FDSourceType.reddit:
|
||||
@@ -105,14 +155,49 @@ extension FDSourceTypeExtension on FDSourceType {
|
||||
return const Color(0xffef8236);
|
||||
case FDSourceType.tumblr:
|
||||
return const Color(0xff34526f);
|
||||
// case FDSourceType.x:
|
||||
// return const Color(0xff000000);
|
||||
case FDSourceType.youtube:
|
||||
return const Color(0xffff0000);
|
||||
default:
|
||||
return Constants.primary;
|
||||
}
|
||||
}
|
||||
|
||||
/// [fgColor] returns the forground color for the source icon. This should be
|
||||
/// used toether with the [bgColor].
|
||||
Color get fgColor {
|
||||
switch (this) {
|
||||
case FDSourceType.fourchan:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.github:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.googlenews:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.lemmy:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.mastodon:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.medium:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.nitter:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.pinterest:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.podcast:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.reddit:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.rss:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.stackoverflow:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.tumblr:
|
||||
return const Color(0xffffffff);
|
||||
case FDSourceType.youtube:
|
||||
return const Color(0xffffffff);
|
||||
default:
|
||||
return Constants.onPrimary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [getSourceTypeFromString] returns the [FDSourceType] from his string
|
||||
@@ -173,7 +258,7 @@ class FDSource {
|
||||
'id': id,
|
||||
'type': type.toShortString(),
|
||||
'title': title,
|
||||
'options': options != null ? options!.toJson() : null,
|
||||
'options': options?.toJson(),
|
||||
'link': link,
|
||||
'icon': icon,
|
||||
};
|
||||
@@ -183,36 +268,44 @@ class FDSource {
|
||||
/// [FDSourceOptions] defines all options for the different source types which
|
||||
/// are available.
|
||||
class FDSourceOptions {
|
||||
String? fourchan;
|
||||
FDGitHubOptions? github;
|
||||
FDGoogleNewsOptions? googlenews;
|
||||
String? lemmy;
|
||||
String? mastodon;
|
||||
String? medium;
|
||||
String? nitter;
|
||||
String? pinterest;
|
||||
String? podcast;
|
||||
String? reddit;
|
||||
String? rss;
|
||||
FDStackOverflowOptions? stackoverflow;
|
||||
String? tumblr;
|
||||
String? x;
|
||||
String? youtube;
|
||||
|
||||
FDSourceOptions({
|
||||
this.fourchan,
|
||||
this.github,
|
||||
this.googlenews,
|
||||
this.lemmy,
|
||||
this.mastodon,
|
||||
this.medium,
|
||||
this.nitter,
|
||||
this.pinterest,
|
||||
this.podcast,
|
||||
this.reddit,
|
||||
this.rss,
|
||||
this.stackoverflow,
|
||||
this.tumblr,
|
||||
this.x,
|
||||
this.youtube,
|
||||
});
|
||||
|
||||
factory FDSourceOptions.fromJson(Map<String, dynamic> responseData) {
|
||||
return FDSourceOptions(
|
||||
fourchan: responseData.containsKey('fourchan') &&
|
||||
responseData['fourchan'] != null
|
||||
? responseData['fourchan']
|
||||
: null,
|
||||
github:
|
||||
responseData.containsKey('github') && responseData['github'] != null
|
||||
? FDGitHubOptions.fromJson(responseData['github'])
|
||||
@@ -221,6 +314,9 @@ class FDSourceOptions {
|
||||
responseData['googlenews'] != null
|
||||
? FDGoogleNewsOptions.fromJson(responseData['googlenews'])
|
||||
: null,
|
||||
lemmy: responseData.containsKey('lemmy') && responseData['lemmy'] != null
|
||||
? responseData['lemmy']
|
||||
: null,
|
||||
mastodon: responseData.containsKey('mastodon') &&
|
||||
responseData['mastodon'] != null
|
||||
? responseData['mastodon']
|
||||
@@ -233,6 +329,10 @@ class FDSourceOptions {
|
||||
responseData.containsKey('nitter') && responseData['nitter'] != null
|
||||
? responseData['nitter']
|
||||
: null,
|
||||
pinterest: responseData.containsKey('pinterest') &&
|
||||
responseData['pinterest'] != null
|
||||
? responseData['pinterest']
|
||||
: null,
|
||||
podcast:
|
||||
responseData.containsKey('podcast') && responseData['podcast'] != null
|
||||
? responseData['podcast']
|
||||
@@ -252,9 +352,6 @@ class FDSourceOptions {
|
||||
responseData.containsKey('tumblr') && responseData['tumblr'] != null
|
||||
? responseData['tumblr']
|
||||
: null,
|
||||
x: responseData.containsKey('x') && responseData['x'] != null
|
||||
? responseData['x']
|
||||
: null,
|
||||
youtube:
|
||||
responseData.containsKey('youtube') && responseData['youtube'] != null
|
||||
? responseData['youtube']
|
||||
@@ -264,17 +361,19 @@ class FDSourceOptions {
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'fourchan': fourchan,
|
||||
'github': github?.toJson(),
|
||||
'googlenews': googlenews?.toJson(),
|
||||
'lemmy': lemmy,
|
||||
'mastodon': mastodon,
|
||||
'medium': medium,
|
||||
'nitter': nitter,
|
||||
'pinterest': pinterest,
|
||||
'podcast': podcast,
|
||||
'reddit': reddit,
|
||||
'rss': rss,
|
||||
'stackoverflow': stackoverflow?.toJson(),
|
||||
'tumblr': tumblr,
|
||||
'x': x,
|
||||
'youtube': youtube,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class AppRepository with ChangeNotifier {
|
||||
.from('decks')
|
||||
.insert({
|
||||
'name': name,
|
||||
'userId': Supabase.instance.client.auth.currentUser!.id
|
||||
'userId': Supabase.instance.client.auth.currentUser!.id,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
@@ -376,21 +376,33 @@ class AppRepository with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// [addSource] is called to add a source to the column with the provided
|
||||
/// [columnId]. The function takes a [source] as parameter. The function calls
|
||||
/// the `add-source-v1` edge function via the Supabase client to create the
|
||||
/// source. When the source was created the newly returned source is added to
|
||||
/// the list of sources of the column with the provided [columnId].
|
||||
/// [columnId]. Next to [columnId] a user must also provide the [type] and
|
||||
/// [options] for the source. The function calls the `add-or-update-source-v1`
|
||||
/// edge function via the Supabase client to create the source. When the
|
||||
/// source was created the newly returned source is added to the list of
|
||||
/// sources of the column with the provided [columnId].
|
||||
///
|
||||
/// The optional [feedData] parameter is used to provide the feed data for the
|
||||
/// source. This is can be used to scrape the source data via the client (app)
|
||||
/// instead of the server (scheduler / worker).
|
||||
Future<void> addSource(
|
||||
String columnId,
|
||||
FDSourceType type,
|
||||
FDSourceOptions options,
|
||||
) async {
|
||||
FDSourceOptions options, [
|
||||
String? feedData,
|
||||
]) async {
|
||||
final result = await Supabase.instance.client.functions.invoke(
|
||||
'add-source-v1',
|
||||
'add-or-update-source-v1',
|
||||
body: {
|
||||
'columnId': columnId,
|
||||
'type': type.toShortString(),
|
||||
'options': options.toJson(),
|
||||
'source': {
|
||||
'id': '',
|
||||
'columnId': columnId,
|
||||
'userId': '',
|
||||
'type': type.toShortString(),
|
||||
'title': '',
|
||||
'options': options.toJson(),
|
||||
},
|
||||
'feedData': feedData,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -105,9 +105,9 @@ class ItemsRepository with ChangeNotifier {
|
||||
Future<void> _init() async {
|
||||
final state = ItemsRepositoryStore().get(column.identifier());
|
||||
if (state != null) {
|
||||
_status = ItemsStatus.loaded;
|
||||
_items.addAll(state.items);
|
||||
_status = state.status;
|
||||
_filters = state.filters;
|
||||
_items.addAll(state.items);
|
||||
notifyListeners();
|
||||
} else {
|
||||
await _getItems();
|
||||
@@ -141,7 +141,7 @@ class ItemsRepository with ChangeNotifier {
|
||||
/// selected source which is stored in the [_filters.sourceIdFilter]
|
||||
/// field.
|
||||
if (_filters.sourceIdFilter != '') {
|
||||
filter = filter.eq('sourceId', sourceIdFilter);
|
||||
filter = filter.eq('sourceId', _filters.sourceIdFilter);
|
||||
}
|
||||
|
||||
filter = filter.lte('createdAt', _filters.createdAtFilter);
|
||||
@@ -168,7 +168,7 @@ class ItemsRepository with ChangeNotifier {
|
||||
/// filter to page through all the items.
|
||||
final data = await filter
|
||||
.order('publishedAt')
|
||||
.range(_filters.offsetFilter, _filters.offsetFilter + 50);
|
||||
.range(_filters.offsetFilter, _filters.offsetFilter + 50 - 1);
|
||||
|
||||
/// The returned items are added to the [_items] field and the status is
|
||||
/// set to [ItemsStatus.loaded] or [ItemsStatus.loadedLast] based on the
|
||||
@@ -189,7 +189,12 @@ class ItemsRepository with ChangeNotifier {
|
||||
/// Finally we store the state of the repository, so that we do not have
|
||||
/// to retrieve the items from the database again when therepository is
|
||||
/// reinitialized.
|
||||
ItemsRepositoryStore().set(column.identifier(), _items, _filters);
|
||||
ItemsRepositoryStore().set(
|
||||
column.identifier(),
|
||||
_status,
|
||||
_filters,
|
||||
_items,
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (_) {
|
||||
_status = ItemsStatus.loaded;
|
||||
@@ -278,7 +283,7 @@ class ItemsRepository with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
ItemsRepositoryStore().set(column.id, _items, _filters);
|
||||
ItemsRepositoryStore().set(column.id, _status, _filters, _items);
|
||||
notifyListeners();
|
||||
} catch (err) {
|
||||
rethrow;
|
||||
@@ -288,18 +293,27 @@ class ItemsRepository with ChangeNotifier {
|
||||
/// [updateReadStates] can be used to mark a list of items provided via their
|
||||
/// [itemIds] as read / unread. When the [read] value is `true` items are
|
||||
/// marked as read and when it is `false` as unread.
|
||||
///
|
||||
/// We have to split the provided list of [itemIds] into chunks of 25 items,
|
||||
/// to avoid the request uri to long error from Supabase. We decided to use 25
|
||||
/// items per chunk, because we think that this is a good tradeoff between the
|
||||
/// number of requests and the number of items we can update at once.
|
||||
Future<void> updateReadStates(List<String> itemIds, bool read) async {
|
||||
try {
|
||||
await Supabase.instance.client
|
||||
.from('items')
|
||||
.update({'isRead': read}).in_('id', itemIds);
|
||||
for (var i = 0; i < _items.length; i++) {
|
||||
if (itemIds.contains(_items[i].id)) {
|
||||
_items[i].isRead = read;
|
||||
final chunks = itemIds.slices(25).toList();
|
||||
|
||||
for (var i = 0; i < chunks.length; i++) {
|
||||
await Supabase.instance.client
|
||||
.from('items')
|
||||
.update({'isRead': read}).inFilter('id', chunks[i]);
|
||||
for (var j = 0; j < _items.length; j++) {
|
||||
if (chunks[i].contains(_items[j].id)) {
|
||||
_items[j].isRead = read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemsRepositoryStore().set(column.id, _items, _filters);
|
||||
ItemsRepositoryStore().set(column.id, _status, _filters, _items);
|
||||
notifyListeners();
|
||||
} catch (err) {
|
||||
rethrow;
|
||||
@@ -321,7 +335,7 @@ class ItemsRepository with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
ItemsRepositoryStore().set(column.id, _items, _filters);
|
||||
ItemsRepositoryStore().set(column.id, _status, _filters, _items);
|
||||
notifyListeners();
|
||||
} catch (err) {
|
||||
rethrow;
|
||||
@@ -333,12 +347,14 @@ class ItemsRepository with ChangeNotifier {
|
||||
/// in the [ItemsRepositoryStore]. The state contains all the loaded [items] and
|
||||
/// the [filters] set by user to load the items.
|
||||
class ItemsRepositoryStoreState {
|
||||
List<FDItem> items;
|
||||
ItemsStatus status;
|
||||
ItemsFilters filters;
|
||||
List<FDItem> items;
|
||||
|
||||
ItemsRepositoryStoreState({
|
||||
required this.items,
|
||||
required this.status,
|
||||
required this.filters,
|
||||
required this.items,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -353,7 +369,7 @@ class ItemsRepositoryStore {
|
||||
static final ItemsRepositoryStore _instance =
|
||||
ItemsRepositoryStore._internal();
|
||||
|
||||
final Map<String, ItemsRepositoryStoreState> itemsRepositoryStoreStates = {};
|
||||
final Map<String, ItemsRepositoryStoreState> _itemsRepositoryStoreStates = {};
|
||||
|
||||
factory ItemsRepositoryStore() {
|
||||
return _instance;
|
||||
@@ -366,7 +382,7 @@ class ItemsRepositoryStore {
|
||||
/// returns `null` to indicate that we have to get the items from the
|
||||
/// database.
|
||||
ItemsRepositoryStoreState? get(String columnId) {
|
||||
return itemsRepositoryStoreStates[columnId];
|
||||
return _itemsRepositoryStoreStates[columnId];
|
||||
}
|
||||
|
||||
/// [set] saves the [items] and [filters] for a [columnId] in the store. This
|
||||
@@ -375,10 +391,23 @@ class ItemsRepositoryStore {
|
||||
///
|
||||
/// The best is to call the [set] function right before we call
|
||||
/// `notifyListeners` in the repository.
|
||||
set(String columnId, List<FDItem> items, ItemsFilters filters) {
|
||||
return itemsRepositoryStoreStates[columnId] = ItemsRepositoryStoreState(
|
||||
items: items,
|
||||
set(
|
||||
String columnId,
|
||||
ItemsStatus status,
|
||||
ItemsFilters filters,
|
||||
List<FDItem> items,
|
||||
) {
|
||||
return _itemsRepositoryStoreStates[columnId] = ItemsRepositoryStoreState(
|
||||
status: status,
|
||||
filters: filters,
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
|
||||
/// [clear] deletes all the stored [_itemsRepositoryStoreStates] from the
|
||||
/// store. This method can be used to clear the cache, e.g. when a user
|
||||
/// changes the active deck or signes out.
|
||||
clear() {
|
||||
_itemsRepositoryStoreStates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
12
app/lib/repositories/layout_repository.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// The [LayoutRepository] is used to store several layout information of the
|
||||
/// app, which can be modifed or should be modifed within different locations.
|
||||
class LayoutRepository with ChangeNotifier {
|
||||
/// [_deckLayoutSmallInitialTabIndex] stores the selected tab index of the
|
||||
/// [DeckLayoutSmall] widget. This is used that we display the same tab when
|
||||
/// a user switches between the small and large layout (e.g. portrait and
|
||||
/// landscape mode on mobile devices) and that we can reset the tab index when
|
||||
/// a user selects a new deck.
|
||||
int deckLayoutSmallInitialTabIndex = 0;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'package:feeddeck/models/profile.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/api_exception.dart';
|
||||
|
||||
enum FDProfileStatus {
|
||||
@@ -13,14 +12,21 @@ enum FDProfileStatus {
|
||||
initialized,
|
||||
}
|
||||
|
||||
/// The [ProfileRepository] is used to fetch and update a users profile data.
|
||||
/// The users profile contains all the required information for the users
|
||||
/// subscription status and the users connected accounts.
|
||||
class ProfileRepository with ChangeNotifier {
|
||||
FDProfileStatus _status = FDProfileStatus.uninitialized;
|
||||
FDProfile? _profile;
|
||||
|
||||
FDProfileStatus get status => _status;
|
||||
FDProfileTier get tier => _profile?.tier ?? FDProfileTier.free;
|
||||
FDProfileSubscriptionProvider? get subscriptionProvider =>
|
||||
_profile?.subscriptionProvider;
|
||||
bool get accountGithub => _profile?.accountGithub ?? false;
|
||||
|
||||
/// [init] is used to fetch the users profile from the `profile-v2` edge
|
||||
/// function.
|
||||
Future<void> init(bool force) async {
|
||||
if (_status == FDProfileStatus.initialized && force == false) {
|
||||
return;
|
||||
@@ -28,7 +34,7 @@ class ProfileRepository with ChangeNotifier {
|
||||
|
||||
try {
|
||||
final result = await Supabase.instance.client.functions.invoke(
|
||||
'profile-v1',
|
||||
'profile-v2/getProfile',
|
||||
method: HttpMethod.get,
|
||||
);
|
||||
|
||||
@@ -45,16 +51,25 @@ class ProfileRepository with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addGithubAccount(String token) async {
|
||||
/// [setTier] is used to update the users tier. We do not have to make an API
|
||||
/// call to update the tier in the database, because this is done via Webhooks
|
||||
/// by the connected payment provider.
|
||||
///
|
||||
/// This is only required to reflect the update in the Flutter app.
|
||||
void setTier(FDProfileTier tier) {
|
||||
_profile?.tier = tier;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// [githubAddAccount] is used to add a GitHub account to the users profile.
|
||||
/// For this the user must provide an private access token with the required
|
||||
/// scopes.
|
||||
Future<void> githubAddAccount(String token) async {
|
||||
final result = await Supabase.instance.client.functions.invoke(
|
||||
'profile-v1',
|
||||
'profile-v2/githubAddAccount',
|
||||
method: HttpMethod.post,
|
||||
body: {
|
||||
'action': 'add-account',
|
||||
'sourceType': FDSourceType.github.toShortString(),
|
||||
'options': {
|
||||
'token': token,
|
||||
}
|
||||
'token': token,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -66,14 +81,12 @@ class ProfileRepository with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> deleteGithubAccount() async {
|
||||
/// [githubDeleteAccount] deletes the users connected GitHub account. For that
|
||||
/// we delete the GitHub access token from the database.
|
||||
Future<void> githubDeleteAccount() async {
|
||||
final result = await Supabase.instance.client.functions.invoke(
|
||||
'profile-v1',
|
||||
method: HttpMethod.post,
|
||||
body: {
|
||||
'action': 'delete-account',
|
||||
'sourceType': FDSourceType.github.toShortString(),
|
||||
},
|
||||
'profile-v2/githubDeleteAccount',
|
||||
method: HttpMethod.delete,
|
||||
);
|
||||
|
||||
if (result.status != 200) {
|
||||
|
||||
@@ -17,7 +17,19 @@ class SettingsRepository {
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Iml0eWp1Y3BzcmFzYXZyaWVwc2NyIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTQwMjY0NjIsImV4cCI6MjAwOTYwMjQ2Mn0.IDo7j9Kh8-5kHLtrZtHTvLf8lUkj7jiLynpIXSZbRFs';
|
||||
String supabaseSiteUrl = 'https://app.feeddeck.app';
|
||||
String googleClientId =
|
||||
'420185423235-9ehth1eodl4lt3cdns7kaf2e89eo6rkq.apps.googleusercontent.com';
|
||||
'296452997087-2o1qasg1gdbe39b1l39dds1doq3t6h2e.apps.googleusercontent.com';
|
||||
|
||||
/// By default the [subscriptionEnabled] variable is set to `true`, so that a
|
||||
/// user can subscribe to FeedDeck Premium. If the variable is set to `false`
|
||||
/// the user can not subscribe to FeedDeck Premium.
|
||||
bool subscriptionEnabled = true;
|
||||
|
||||
/// The [revenueCatAppStoreKey] and [revenueCatGooglePlayKey] are used for the
|
||||
/// in-app purchases. The [revenueCatAppStoreKey] is used for the Apple App
|
||||
/// Store on iOS and macOS. The [revenueCatGooglePlayKey] is used for the
|
||||
/// Google Play Store on Android.
|
||||
final String revenueCatAppStoreKey = 'appl_kThbIaMkylDBtCEmsfczvgCBram';
|
||||
final String revenueCatGooglePlayKey = 'goog_tBFPbLbygjioviXRIlGlmUOKZYA';
|
||||
|
||||
factory SettingsRepository() {
|
||||
return _instance;
|
||||
@@ -52,11 +64,16 @@ class SettingsRepository {
|
||||
googleClientIdPrefs != null) {
|
||||
/// Store the user provided values within the [SettingsRepository] and
|
||||
/// use them to initialize the Supabase client.
|
||||
///
|
||||
/// Also set the [subscriptionEnabled] variabel to `false`, so that the
|
||||
/// user can not subscribe to FeedDeck Premium.
|
||||
supabaseUrl = supabaseUrlPrefs;
|
||||
supabaseAnonKey = supabaseAnonKeyPrefs;
|
||||
supabaseSiteUrl = supabaseSiteUrlPrefs;
|
||||
googleClientId = googleClientIdPrefs;
|
||||
|
||||
subscriptionEnabled = false;
|
||||
|
||||
await Supabase.initialize(
|
||||
url: supabaseUrlPrefs,
|
||||
anonKey: supabaseAnonKeyPrefs,
|
||||
|
||||
@@ -56,15 +56,15 @@ const _htmlAuthFinished = '''
|
||||
</html>
|
||||
''';
|
||||
|
||||
/// The [DesktopLoginManager] is used to authenticate a user with the provided
|
||||
/// The [DesktopSignInManager] is used to authenticate a user with the provided
|
||||
/// OAuth [provider] on desktop platforms.
|
||||
class DesktopLoginManager {
|
||||
final supabase.Provider provider;
|
||||
class DesktopSignInManager {
|
||||
final supabase.OAuthProvider provider;
|
||||
final Map<String, String>? queryParams;
|
||||
|
||||
HttpServer? redirectServer;
|
||||
|
||||
DesktopLoginManager({
|
||||
DesktopSignInManager({
|
||||
required this.provider,
|
||||
required this.queryParams,
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Flutter icons FDIcons
|
||||
/// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com
|
||||
/// Copyright (C) 2024 by original authors @ fluttericon.com, fontello.com
|
||||
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
|
||||
///
|
||||
/// To use this font, place it in your fonts/ directory and include the
|
||||
@@ -11,11 +13,6 @@
|
||||
/// fonts:
|
||||
/// - asset: fonts/FDIcons.ttf
|
||||
///
|
||||
///
|
||||
///
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class FDIcons {
|
||||
FDIcons._();
|
||||
|
||||
@@ -32,6 +29,8 @@ class FDIcons {
|
||||
IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData googlenews =
|
||||
IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData pinterest =
|
||||
IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData medium =
|
||||
IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData nitter =
|
||||
@@ -64,4 +63,8 @@ class FDIcons {
|
||||
IconData(0xe814, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData mastodon =
|
||||
IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData lemmy =
|
||||
IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
static const IconData fourchan =
|
||||
IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||
}
|
||||
|
||||
55
app/lib/utils/get_feed.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/api_exception.dart';
|
||||
|
||||
/// [getFeed] returns the feed for the provided [sourceType] and [options]. It
|
||||
/// can be used to fetch the feed for a source on the client side (app) instead
|
||||
/// of via the corresponding `add-or-update-source-v1` edge function or via our
|
||||
/// worker.
|
||||
///
|
||||
/// The functions for the different sources must implement the same parsing for
|
||||
/// the source options as it is done in the edge function.
|
||||
Future<String> getFeed(FDSourceType sourceType, FDSourceOptions options) async {
|
||||
switch (sourceType) {
|
||||
case FDSourceType.reddit:
|
||||
return getFeedReddit(options.reddit);
|
||||
default:
|
||||
throw const ApiException('Unknown source type', 400);
|
||||
}
|
||||
}
|
||||
|
||||
/// [getFeedReddit] returns the feed for the provided [input]. It is used to
|
||||
/// fetch the RSS feed for a Reddit source, which can be passed to the
|
||||
/// `add-or-update-source-v1` edge function.
|
||||
///
|
||||
/// The function must implement the same parsing logic as it is done in the
|
||||
/// `supabase/functions/_shared/feed/reddit.ts` file.
|
||||
Future<String> getFeedReddit(String? input) async {
|
||||
if (input == null || input.isEmpty) {
|
||||
throw const ApiException('No input provided', 400);
|
||||
}
|
||||
|
||||
String url = '';
|
||||
try {
|
||||
if (input.startsWith('/r/') || input.startsWith('/u/')) {
|
||||
url = 'https://www.reddit.com$input.rss';
|
||||
} else {
|
||||
final inputUri = Uri.parse(input);
|
||||
if (inputUri.host.endsWith('reddit.com')) {
|
||||
if (input.endsWith('.rss')) {
|
||||
url = input;
|
||||
} else {
|
||||
url = '$input.rss';
|
||||
}
|
||||
} else {
|
||||
throw const ApiException('Invalid input', 400);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw const ApiException('Invalid input', 400);
|
||||
}
|
||||
|
||||
final response = await http.get(Uri.parse(url));
|
||||
return response.body;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'package:feeddeck/repositories/settings_repository.dart';
|
||||
|
||||
/// [FDImageType] is a enum value which defines the image type. An image can be
|
||||
/// related to an item or a source.
|
||||
enum FDImageType {
|
||||
item,
|
||||
source,
|
||||
}
|
||||
|
||||
/// [getImageUrl] returns the correct image url to use for the provided image
|
||||
/// url:
|
||||
/// - If the [imageType] is [FDImageType.source] the image is always requested
|
||||
/// from the Supabase storage.
|
||||
/// - If the [imageType] is [FDImageType.item] and the app runs on the web, the
|
||||
/// image is proxied through the Supabase functions.
|
||||
/// - If the [imageType] is [FDImageType.item] and the app runs on a mobile or
|
||||
/// desktop device, the image is requested directly from the provided url.
|
||||
String getImageUrl(FDImageType imageType, String imageUrl) {
|
||||
if (imageType == FDImageType.source) {
|
||||
return '${SettingsRepository().supabaseUrl}/storage/v1/object/public/sources/$imageUrl';
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
return '${Supabase.instance.client.functionsUrl}/image-proxy-v1?media=${Uri.encodeQueryComponent(imageUrl)}';
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
33
app/lib/utils/signin_with_apple.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
/// [signInWithApple] performs Apple sign in on iOS and macOS.
|
||||
/// See https://supabase.com/docs/guides/auth/social-login/auth-apple?platform=flutter#using-native-sign-in-with-apple-in-flutter
|
||||
Future<AuthResponse> signInWithApple() async {
|
||||
final rawNonce = Supabase.instance.client.auth.generateRawNonce();
|
||||
final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();
|
||||
|
||||
final credential = await SignInWithApple.getAppleIDCredential(
|
||||
scopes: [
|
||||
AppleIDAuthorizationScopes.email,
|
||||
AppleIDAuthorizationScopes.fullName,
|
||||
],
|
||||
nonce: hashedNonce,
|
||||
);
|
||||
|
||||
final idToken = credential.identityToken;
|
||||
if (idToken == null) {
|
||||
throw const AuthException(
|
||||
'Could not find ID Token from generated credential.',
|
||||
);
|
||||
}
|
||||
|
||||
return Supabase.instance.client.auth.signInWithIdToken(
|
||||
provider: OAuthProvider.apple,
|
||||
idToken: idToken,
|
||||
nonce: rawNonce,
|
||||
);
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class _ColumnLayoutHeaderState extends State<ColumnLayoutHeader> {
|
||||
),
|
||||
Text(
|
||||
Characters(
|
||||
'${items.column.sources.length} ${items.column.sources.length == 1 ? 'Source' : 'Sources'} / ${items.items.length} ${items.items.length == 1 ? 'Item' : 'Items'}',
|
||||
'${items.column.sources.length} ${items.column.sources.length == 1 ? 'Source' : 'Sources'} / ${items.items.length}${items.status == ItemsStatus.loaded ? '+' : ''} ${items.items.length == 1 ? 'Item' : 'Items'}',
|
||||
)
|
||||
.replaceAll(
|
||||
Characters(''),
|
||||
|
||||
@@ -100,7 +100,7 @@ class ColumnLayoutHeaderSettings extends StatelessWidget {
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ class _ColumnLayoutHeaderSettingsDeleteColumnState
|
||||
await Provider.of<AppRepository>(context, listen: false)
|
||||
.deleteColumn(widget.column.id);
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
duration: Duration(seconds: 10),
|
||||
|
||||
@@ -112,6 +112,64 @@ class _CreateDeckState extends State<CreateDeck> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: const TextSpan(
|
||||
style: TextStyle(
|
||||
color: Constants.onSurface,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'We\'re excited to have you onboard at ',
|
||||
),
|
||||
TextSpan(
|
||||
text: 'FeedDeck',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
'. We hope you enjoy your journey with us. If you have any questions or need assistance, feel free to reach out.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: const TextSpan(
|
||||
style: TextStyle(
|
||||
color: Constants.onSurface,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
'Now it\'s time to create your first deck. A deck is a collection of columns and sources. Provide the name for your first deck and click on the ',
|
||||
),
|
||||
TextSpan(
|
||||
text: '"Create Deck"',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: ' button.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
keyboardType: TextInputType.text,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/repositories/app_repository.dart';
|
||||
import 'package:feeddeck/repositories/layout_repository.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/column/column_layout.dart';
|
||||
import 'package:feeddeck/widgets/column/create/create_column.dart';
|
||||
@@ -195,9 +196,9 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
/// The drawer is used to display the [CreateColumn] widget, so that a
|
||||
/// user can add a new column without leaving the screen.
|
||||
drawer: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(Constants.spacingMiddle),
|
||||
bottom: Radius.circular(Constants.spacingMiddle),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(Constants.spacingMiddle),
|
||||
bottomRight: Radius.circular(Constants.spacingMiddle),
|
||||
),
|
||||
child: Drawer(
|
||||
width: Constants.columnWidth,
|
||||
@@ -224,10 +225,20 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
backgroundColor: Constants.background,
|
||||
selectedIndex: null,
|
||||
|
||||
/// When a user selects a destination in the navigation rail
|
||||
/// we scroll to the corresponding column by using the
|
||||
/// `scroll_to_index` package.
|
||||
/// When a user selects a destination in the navigation
|
||||
/// rail we scroll to the corresponding column by using
|
||||
/// the `scroll_to_index` package.
|
||||
onDestinationSelected: (int index) {
|
||||
/// Before we scroll to the corresponding column, we
|
||||
/// also update the [deckLayoutSmallInitialTabIndex] in
|
||||
/// the [LayoutRepository] so that the correct tab is
|
||||
/// also selected when a user switches to the small
|
||||
/// layout.
|
||||
Provider.of<LayoutRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).deckLayoutSmallInitialTabIndex = index;
|
||||
|
||||
_scrollController.scrollToIndex(
|
||||
index,
|
||||
preferPosition: AutoScrollPosition.end,
|
||||
@@ -244,8 +255,8 @@ class _DeckLayoutLargeState extends State<DeckLayoutLarge> {
|
||||
|
||||
/// We add two additional items to the navigation rail via
|
||||
/// the trailing property. These items are used to allow a
|
||||
/// user to create a new column and to go to the settings of
|
||||
/// the app.
|
||||
/// user to create a new column and to go to the settings
|
||||
/// of the app.
|
||||
trailing: Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/repositories/app_repository.dart';
|
||||
import 'package:feeddeck/repositories/layout_repository.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/column/column_layout.dart';
|
||||
import 'package:feeddeck/widgets/column/create/create_column.dart';
|
||||
@@ -20,6 +21,30 @@ import 'package:feeddeck/widgets/source/source_icon.dart';
|
||||
class DeckLayoutSmall extends StatelessWidget {
|
||||
const DeckLayoutSmall({super.key});
|
||||
|
||||
/// [_getInitialIndex] returns the initial index for the
|
||||
/// [DefaultTabController]. The intial index is saved in the
|
||||
/// [LayoutRepository] so that we can use it when a user switches between the
|
||||
/// small and large layout or between decks. To not run into errors we have to
|
||||
/// check that a column which should be used was not already deleted by a
|
||||
/// user before returning the index. If a column was deleted we reset the
|
||||
/// index to 0.
|
||||
int _getInitialIndex(BuildContext context, int columnsLength) {
|
||||
final deckLayoutSmallInitialTabIndex = Provider.of<LayoutRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).deckLayoutSmallInitialTabIndex;
|
||||
|
||||
if (deckLayoutSmallInitialTabIndex >= columnsLength) {
|
||||
Provider.of<LayoutRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).deckLayoutSmallInitialTabIndex = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return deckLayoutSmallInitialTabIndex;
|
||||
}
|
||||
|
||||
/// [_buildTabs] returns all items for the tab bar. The items are generated
|
||||
/// by looping thorugh the `columns` defined in the [AppRepository].
|
||||
///
|
||||
@@ -124,6 +149,8 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
AppRepository app = Provider.of<AppRepository>(context, listen: true);
|
||||
|
||||
return DefaultTabController(
|
||||
key: ValueKey(app.activeDeckId),
|
||||
initialIndex: _getInitialIndex(context, app.columns.length),
|
||||
length: app.columns.length,
|
||||
child: Scaffold(
|
||||
bottomNavigationBar: SafeArea(
|
||||
@@ -148,6 +175,18 @@ class DeckLayoutSmall extends StatelessWidget {
|
||||
),
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
dividerHeight: 0,
|
||||
onTap: (int index) {
|
||||
/// When the user clicks on a tab we update the index in
|
||||
/// the [LayoutRepository] so that we can use it as
|
||||
/// initial index when the widget is rebuild (e.g. when
|
||||
/// a user switches between the large and small layout).
|
||||
Provider.of<LayoutRepository>(
|
||||
context,
|
||||
listen: false,
|
||||
).deckLayoutSmallInitialTabIndex = index;
|
||||
},
|
||||
tabs: _buildTabs(context),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -24,7 +24,7 @@ import 'package:flutter/material.dart';
|
||||
/// ),
|
||||
/// )
|
||||
class ElevatedButtonProgressIndicator extends StatelessWidget {
|
||||
const ElevatedButtonProgressIndicator({Key? key}) : super(key: key);
|
||||
const ElevatedButtonProgressIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -39,7 +39,7 @@ class _HomeState extends State<Home> {
|
||||
});
|
||||
|
||||
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||
_appLinks.allUriLinkStream.listen((uri) {
|
||||
_appLinks.uriLinkStream.listen((uri) {
|
||||
if (uri
|
||||
.toString()
|
||||
.startsWith('app.feeddeck.feeddeck://signin-callback/')) {
|
||||
|
||||
@@ -7,9 +7,12 @@ import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/repositories/items_repository.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/utils/openurl.dart';
|
||||
import 'package:feeddeck/widgets/item/details/item_details_fourchan.dart';
|
||||
import 'package:feeddeck/widgets/item/details/item_details_lemmy.dart';
|
||||
import 'package:feeddeck/widgets/item/details/item_details_mastodon.dart';
|
||||
import 'package:feeddeck/widgets/item/details/item_details_medium.dart';
|
||||
import 'package:feeddeck/widgets/item/details/item_details_nitter.dart';
|
||||
import 'package:feeddeck/widgets/item/details/item_details_pinterest.dart';
|
||||
import 'package:feeddeck/widgets/item/details/item_details_podcast.dart';
|
||||
import 'package:feeddeck/widgets/item/details/item_details_reddit.dart';
|
||||
import 'package:feeddeck/widgets/item/details/item_details_rss.dart';
|
||||
@@ -59,6 +62,12 @@ class ItemDetails extends StatelessWidget {
|
||||
|
||||
Widget _buildDetails() {
|
||||
switch (source.type) {
|
||||
case FDSourceType.fourchan:
|
||||
return ItemDetailsFourChan(
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
|
||||
/// Sources with type [FDSourceType.github] do not provide a details view,
|
||||
/// because we directly open the link, when the user clicks on the
|
||||
/// corresponding preview item.
|
||||
@@ -70,6 +79,11 @@ class ItemDetails extends StatelessWidget {
|
||||
/// corresponding preview item.
|
||||
case FDSourceType.googlenews:
|
||||
return Container();
|
||||
case FDSourceType.lemmy:
|
||||
return ItemDetailsLemmy(
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
case FDSourceType.mastodon:
|
||||
return ItemDetailsMastodon(
|
||||
item: item,
|
||||
@@ -85,6 +99,11 @@ class ItemDetails extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
case FDSourceType.pinterest:
|
||||
return ItemDetailsPinterest(
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
case FDSourceType.podcast:
|
||||
return ItemDetailsPodcast(
|
||||
item: item,
|
||||
@@ -110,11 +129,6 @@ class ItemDetails extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
);
|
||||
// case FDSourceType.x:
|
||||
// return ItemDetailsX(
|
||||
// item: item,
|
||||
// source: source,
|
||||
// );
|
||||
case FDSourceType.youtube:
|
||||
return ItemDetailsYoutube(
|
||||
item: item,
|
||||
@@ -196,7 +210,7 @@ class ItemDetails extends StatelessWidget {
|
||||
Constants.elevatedButtonSize,
|
||||
),
|
||||
),
|
||||
label: const Text('Open link'),
|
||||
label: const Text('Open Link'),
|
||||
onPressed: () => _openUrl(item.link),
|
||||
icon: const Icon(Icons.launch),
|
||||
),
|
||||
|
||||
@@ -2,13 +2,12 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_media_gallery.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
|
||||
|
||||
class ItemDetailsX extends StatelessWidget {
|
||||
const ItemDetailsX({
|
||||
class ItemDetailsFourChan extends StatelessWidget {
|
||||
const ItemDetailsFourChan({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.source,
|
||||
@@ -23,6 +22,9 @@ class ItemDetailsX extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ItemTitle(
|
||||
itemTitle: item.title,
|
||||
),
|
||||
ItemSubtitle(
|
||||
item: item,
|
||||
source: source,
|
||||
@@ -32,16 +34,6 @@ class ItemDetailsX extends StatelessWidget {
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingExtraSmall,
|
||||
),
|
||||
ItemMediaGallery(
|
||||
itemMedias: item.options != null && item.options!.containsKey('media')
|
||||
? (item.options!['media'] as List)
|
||||
.map((item) => item as String)
|
||||
.toList()
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
90
app/lib/widgets/item/details/item_details_lemmy.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_piped/item_piped_video.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_videos.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
|
||||
|
||||
class ItemDetailsLemmy extends StatelessWidget {
|
||||
const ItemDetailsLemmy({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
/// [_buildMedia] builds the media widget for the item. The media widget can
|
||||
/// display an image, a video or y YouTube video.
|
||||
///
|
||||
/// See the `getMedia` function in the `lemmy.ts` file, for a list of
|
||||
/// extension which are a image / video.
|
||||
Widget _buildMedia() {
|
||||
if (item.media != null && item.media! != '') {
|
||||
final mediaUrl = Uri.parse(item.media!);
|
||||
|
||||
if (mediaUrl.path.endsWith('.jpg') ||
|
||||
mediaUrl.path.endsWith('.jpeg') ||
|
||||
mediaUrl.path.endsWith('.png') ||
|
||||
mediaUrl.path.endsWith('.gif')) {
|
||||
return ItemMedia(
|
||||
itemMedia: item.media,
|
||||
);
|
||||
}
|
||||
|
||||
if (mediaUrl.path.endsWith('.mp4')) {
|
||||
return ItemVideoPlayer(
|
||||
video: item.media!,
|
||||
);
|
||||
}
|
||||
|
||||
if (item.media!.startsWith('https://youtu.be/') ||
|
||||
item.media!.startsWith('https://www.youtube.com/watch?') ||
|
||||
item.media!.startsWith('https://m.youtube.com/watch?')) {
|
||||
return ItemYoutubeVideo(
|
||||
null,
|
||||
item.media!,
|
||||
);
|
||||
}
|
||||
|
||||
if (item.media!.startsWith('https://piped.video/watch?v=') ||
|
||||
item.media!.startsWith('https://piped.video/')) {
|
||||
return ItemPipedVideo(
|
||||
null,
|
||||
item.media!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ItemTitle(
|
||||
itemTitle: item.title,
|
||||
),
|
||||
ItemSubtitle(
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
_buildMedia(),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_media_gallery.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_piped/item_piped_video.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_videos.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
|
||||
|
||||
class ItemDetailsMastodon extends StatelessWidget {
|
||||
const ItemDetailsMastodon({
|
||||
@@ -17,6 +20,116 @@ class ItemDetailsMastodon extends StatelessWidget {
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
/// [_getYoutubeUrl] returns a YouTube url when the provided [description]
|
||||
/// contains a YouTube link. If the [description] does not contain a YouTube
|
||||
/// link, the function returns `null`.
|
||||
String? _getYoutubeUrl(String description) {
|
||||
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
|
||||
final matches = exp.allMatches(description);
|
||||
|
||||
for (var match in matches) {
|
||||
final url = description.substring(match.start, match.end);
|
||||
if (url.startsWith('https://youtu.be/') ||
|
||||
url.startsWith('https://www.youtube.com/watch?') ||
|
||||
url.startsWith('https://m.youtube.com/watch?')) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [_getPipedUrl] returns a Piped url when the provided [description]
|
||||
/// contains a Piped link. If the [description] does not contain a Piped link,
|
||||
/// the function returns `null`.
|
||||
String? _getPipedUrl(String description) {
|
||||
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
|
||||
final matches = exp.allMatches(description);
|
||||
|
||||
for (var match in matches) {
|
||||
final url = description.substring(match.start, match.end);
|
||||
if (url.startsWith('https://piped.video/watch?v=') ||
|
||||
url.startsWith('https://piped.video/')) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [_buildDescription] builds the description widget for the item. If the
|
||||
/// description contains a YouTube link, we render the [ItemYoutubeVideo]
|
||||
/// and the [ItemDescription] widgets. If the description does not contain a
|
||||
/// YouTube link, we render the [ItemDescription], [ItemMediaGallery] and
|
||||
/// [ItemVideos] widget.
|
||||
List<Widget> _buildDescription() {
|
||||
final youtubeUrl =
|
||||
item.description != null ? _getYoutubeUrl(item.description!) : null;
|
||||
|
||||
if (youtubeUrl != null) {
|
||||
return [
|
||||
ItemYoutubeVideo(
|
||||
item.media,
|
||||
youtubeUrl,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
disableImages: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
final pipedUrl =
|
||||
item.description != null ? _getPipedUrl(item.description!) : null;
|
||||
|
||||
if (pipedUrl != null) {
|
||||
return [
|
||||
ItemPipedVideo(
|
||||
item.media,
|
||||
pipedUrl,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
disableImages: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
disableImages: true,
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingExtraSmall,
|
||||
),
|
||||
ItemMediaGallery(
|
||||
itemMedias: item.options != null &&
|
||||
item.options!.containsKey('media') &&
|
||||
item.options!['media'] != null
|
||||
? (item.options!['media'] as List)
|
||||
.map((item) => item as String)
|
||||
.toList()
|
||||
: null,
|
||||
),
|
||||
ItemVideos(
|
||||
videos: item.options != null &&
|
||||
item.options!.containsKey('videos') &&
|
||||
item.options!['videos'] != null
|
||||
? (item.options!['videos'] as List)
|
||||
.map((item) => item as String)
|
||||
.toList()
|
||||
: null,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -27,21 +140,7 @@ class ItemDetailsMastodon extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingExtraSmall,
|
||||
),
|
||||
ItemMediaGallery(
|
||||
itemMedias: item.options != null && item.options!.containsKey('media')
|
||||
? (item.options!['media'] as List)
|
||||
.map((item) => item as String)
|
||||
.toList()
|
||||
: null,
|
||||
)
|
||||
..._buildDescription(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_media_gallery.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_piped/item_piped_video.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
|
||||
|
||||
class ItemDetailsNitter extends StatelessWidget {
|
||||
const ItemDetailsNitter({
|
||||
@@ -18,18 +18,37 @@ class ItemDetailsNitter extends StatelessWidget {
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ItemTitle(
|
||||
itemTitle: item.title,
|
||||
),
|
||||
ItemSubtitle(
|
||||
item: item,
|
||||
source: source,
|
||||
/// [_getPipedUrl] returns a Piped url when the provided [description]
|
||||
/// contains a Piped link. If the [description] does not contain a Piped link,
|
||||
/// the function returns `null`.
|
||||
String? _getPipedUrl(String description) {
|
||||
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
|
||||
final matches = exp.allMatches(description);
|
||||
|
||||
for (var match in matches) {
|
||||
final url = description.substring(match.start, match.end);
|
||||
if (url.startsWith('https://piped.video/watch?v=') ||
|
||||
url.startsWith('https://piped.video/')) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [_buildDescription] builds the description widget for the item. If the
|
||||
/// description contains a Piped link, we render the [ItemPipedVideo] and the
|
||||
/// [ItemDescription] widgets. If the description does not contain a Piped
|
||||
/// link, we render the [ItemDescription] and [ItemMediaGallery] widget.
|
||||
List<Widget> _buildDescription() {
|
||||
final pipedUrl =
|
||||
item.description != null ? _getPipedUrl(item.description!) : null;
|
||||
|
||||
if (pipedUrl != null) {
|
||||
return [
|
||||
ItemPipedVideo(
|
||||
item.media,
|
||||
pipedUrl,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
@@ -37,16 +56,40 @@ class ItemDetailsNitter extends StatelessWidget {
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
disableImages: true,
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingExtraSmall,
|
||||
),
|
||||
ItemMediaGallery(
|
||||
itemMedias: item.options != null && item.options!.containsKey('media')
|
||||
? (item.options!['media'] as List)
|
||||
.map((item) => item as String)
|
||||
.toList()
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
disableImages: true,
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingExtraSmall,
|
||||
),
|
||||
ItemMediaGallery(
|
||||
itemMedias: item.options != null && item.options!.containsKey('media')
|
||||
? (item.options!['media'] as List)
|
||||
.map((item) => item as String)
|
||||
.toList()
|
||||
: null,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ItemSubtitle(
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
..._buildDescription(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
40
app/lib/widgets/item/details/item_details_pinterest.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
|
||||
|
||||
class ItemDetailsPinterest extends StatelessWidget {
|
||||
const ItemDetailsPinterest({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ItemTitle(
|
||||
itemTitle: item.title,
|
||||
),
|
||||
ItemSubtitle(
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/utils/image_url.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_audio_palyer/item_audio_player.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
|
||||
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
|
||||
|
||||
class ItemDetailsPodcast extends StatelessWidget {
|
||||
const ItemDetailsPodcast({
|
||||
@@ -21,14 +19,6 @@ class ItemDetailsPodcast extends StatelessWidget {
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
String? _buildImageUrl() {
|
||||
if (source.icon != null && source.icon != '') {
|
||||
return getImageUrl(FDImageType.source, source.icon!);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [_buildImage] returns an image which is displayed above the
|
||||
/// [ItemAudioPlayer]. For this image we are using the [source.icon] if it is
|
||||
/// available. If the [source.icon] is not available, we are not displaying
|
||||
@@ -55,8 +45,6 @@ class ItemDetailsPodcast extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageUrl = _buildImageUrl();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -74,7 +62,7 @@ class ItemDetailsPodcast extends StatelessWidget {
|
||||
/// directly listen to the podcast episode.
|
||||
Column(
|
||||
children: [
|
||||
_buildImage(imageUrl),
|
||||
_buildImage(source.icon),
|
||||
const SizedBox(
|
||||
height: Constants.spacingMiddle,
|
||||
),
|
||||
@@ -83,7 +71,7 @@ class ItemDetailsPodcast extends StatelessWidget {
|
||||
audioFile: item.media!,
|
||||
audioTitle: item.title,
|
||||
audioArtist: source.title,
|
||||
audioArt: imageUrl,
|
||||
audioArt: getImageUrl(source.icon!),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Constants.spacingMiddle,
|
||||
|
||||
@@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_piped/item_piped_video.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
|
||||
|
||||
class ItemDetailsReddit extends StatelessWidget {
|
||||
const ItemDetailsReddit({
|
||||
@@ -16,6 +18,98 @@ class ItemDetailsReddit extends StatelessWidget {
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
/// [_getYoutubeUrl] returns a YouTube url when the provided [description]
|
||||
/// contains a YouTube link. If the [description] does not contain a YouTube
|
||||
/// link, the function returns `null`.
|
||||
String? _getYoutubeUrl(String description) {
|
||||
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
|
||||
final matches = exp.allMatches(description);
|
||||
|
||||
for (var match in matches) {
|
||||
final url = description.substring(match.start, match.end);
|
||||
if (url.startsWith('https://youtu.be/') ||
|
||||
url.startsWith('https://www.youtube.com/watch?') ||
|
||||
url.startsWith('https://m.youtube.com/watch?')) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [_getPipedUrl] returns a Piped url when the provided [description]
|
||||
/// contains a Piped link. If the [description] does not contain a Piped link,
|
||||
/// the function returns `null`.
|
||||
String? _getPipedUrl(String description) {
|
||||
final exp = RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
|
||||
final matches = exp.allMatches(description);
|
||||
|
||||
for (var match in matches) {
|
||||
final url = description.substring(match.start, match.end);
|
||||
if (url.startsWith('https://piped.video/watch?v=') ||
|
||||
url.startsWith('https://piped.video/')) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// [_buildDescription] builds the description widget for the item. If the
|
||||
/// description contains a YouTube link, we render the [ItemYoutubeVideo]
|
||||
/// and the [ItemDescription] widgets. If the description contains a Piped
|
||||
/// link, we render the [ItemPipedVideo] and the [ItemDescription] widget. If
|
||||
/// the description does not contain a YouTube or Piped link, we only render
|
||||
/// the [ItemDescription] widget.
|
||||
///
|
||||
/// If the description containes a YouTube link we also have to disable the
|
||||
/// rendering of images within the [ItemDescription] widget.
|
||||
List<Widget> _buildDescription() {
|
||||
final youtubeUrl =
|
||||
item.description != null ? _getYoutubeUrl(item.description!) : null;
|
||||
|
||||
if (youtubeUrl != null) {
|
||||
return [
|
||||
ItemYoutubeVideo(
|
||||
item.media,
|
||||
youtubeUrl,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
disableImages: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
final pipedUrl =
|
||||
item.description != null ? _getPipedUrl(item.description!) : null;
|
||||
|
||||
if (pipedUrl != null) {
|
||||
return [
|
||||
ItemPipedVideo(
|
||||
item.media,
|
||||
pipedUrl,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
disableImages: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -29,11 +123,7 @@ class ItemDetailsReddit extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
..._buildDescription(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:html/parser.dart' show parse;
|
||||
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_videos.dart';
|
||||
|
||||
class ItemDetailsRSS extends StatelessWidget {
|
||||
const ItemDetailsRSS({
|
||||
@@ -17,6 +20,31 @@ class ItemDetailsRSS extends StatelessWidget {
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
/// [_buildMedia] renders an image or video for the item. If the description
|
||||
/// of the item contains an image we do not render the image, because it could
|
||||
/// already be rendered via the description.
|
||||
///
|
||||
/// Videos are currently always rendered, because they will not be rendered,
|
||||
/// by the [MarkdownBody] widget.
|
||||
Widget _buildMedia() {
|
||||
if (item.options != null &&
|
||||
item.options!.containsKey('video') &&
|
||||
item.options!['video'] != null) {
|
||||
return ItemVideos(videos: [item.options!['video']]);
|
||||
}
|
||||
|
||||
/// Check if the description of the RSS feed contains an image. If this is
|
||||
/// the case we do not render the image from the [item.media] because the
|
||||
/// image is already rendered in the [ItemDescription] widget.
|
||||
if (parse(item.description).querySelectorAll('img').isNotEmpty) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return ItemMedia(
|
||||
itemMedia: item.media,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -30,13 +58,11 @@ class ItemDetailsRSS extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
ItemMedia(
|
||||
itemMedia: item.media,
|
||||
),
|
||||
_buildMedia(),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.plain,
|
||||
tagetFormat: DescriptionFormat.plain,
|
||||
sourceFormat: DescriptionFormat.html,
|
||||
tagetFormat: DescriptionFormat.markdown,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:youtube_player_iframe/youtube_player_iframe.dart';
|
||||
|
||||
import 'package:feeddeck/models/item.dart';
|
||||
import 'package:feeddeck/models/source.dart';
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_media.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
|
||||
import 'package:feeddeck/widgets/item/details/utils/item_youtube/item_youtube_video.dart';
|
||||
|
||||
class ItemDetailsYoutube extends StatelessWidget {
|
||||
const ItemDetailsYoutube({
|
||||
@@ -23,22 +17,6 @@ class ItemDetailsYoutube extends StatelessWidget {
|
||||
final FDItem item;
|
||||
final FDSource source;
|
||||
|
||||
/// [_buildMedia] returns the media element for the item. On the web we are
|
||||
/// using the [YoutubeVideo] widget to render the video, so that a user can
|
||||
/// directly play the YouTube video. On all other platforms we display the
|
||||
/// thumbnail of the video.
|
||||
Widget _buildMedia() {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
|
||||
return YoutubeVideo(
|
||||
videoUrl: item.link,
|
||||
);
|
||||
}
|
||||
|
||||
return ItemMedia(
|
||||
itemMedia: item.media,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -52,7 +30,10 @@ class ItemDetailsYoutube extends StatelessWidget {
|
||||
item: item,
|
||||
source: source,
|
||||
),
|
||||
_buildMedia(),
|
||||
ItemYoutubeVideo(
|
||||
item.media,
|
||||
item.link,
|
||||
),
|
||||
ItemDescription(
|
||||
itemDescription: item.description,
|
||||
sourceFormat: DescriptionFormat.plain,
|
||||
@@ -62,51 +43,3 @@ class ItemDetailsYoutube extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class YoutubeVideo extends StatefulWidget {
|
||||
const YoutubeVideo({
|
||||
super.key,
|
||||
required this.videoUrl,
|
||||
});
|
||||
|
||||
final String videoUrl;
|
||||
|
||||
@override
|
||||
State<YoutubeVideo> createState() => _YoutubeVideoState();
|
||||
}
|
||||
|
||||
class _YoutubeVideoState extends State<YoutubeVideo> {
|
||||
late YoutubePlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = YoutubePlayerController(
|
||||
params: const YoutubePlayerParams(
|
||||
showControls: true,
|
||||
showFullscreenButton: false,
|
||||
),
|
||||
);
|
||||
|
||||
_controller.cueVideoByUrl(mediaContentUrl: widget.videoUrl);
|
||||
_controller.cueVideoById(
|
||||
videoId: widget.videoUrl.replaceFirst(
|
||||
'https://www.youtube.com/watch?v=',
|
||||
'',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
),
|
||||
child: YoutubePlayer(
|
||||
controller: _controller,
|
||||
aspectRatio: 16 / 9,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,13 @@ class _ItemAudioPlayerState extends State<ItemAudioPlayer> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
/// We have to dispose the [_player] when the widget is disposed, otherwise
|
||||
/// the audio will continue to play in the background.
|
||||
///
|
||||
/// On Linux and Windows the audio will continue to play even if the
|
||||
/// [_player] is disposed, so that we also call the `pause` method of the
|
||||
/// [_player] to stop the audio.
|
||||
_player.pause();
|
||||
_player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -67,15 +67,8 @@ class ItemAudioPlayerControlButtons extends StatelessWidget {
|
||||
final playerState = snapshot.data;
|
||||
final processingState = playerState?.processingState;
|
||||
final playing = playerState?.playing;
|
||||
if (processingState == ProcessingState.loading ||
|
||||
processingState == ProcessingState.buffering) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(Constants.spacingSmall),
|
||||
width: 48.0,
|
||||
height: 48.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
} else if (playing != true) {
|
||||
|
||||
if (playing != true) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
iconSize: 64.0,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
library item_audio_player_init;
|
||||
|
||||
import 'item_audio_player_init_stub.dart'
|
||||
if (dart.library.io) 'item_audio_player_init_native.dart'
|
||||
if (dart.library.html) 'item_audio_player_init_web.dart';
|
||||
|
||||
abstract class ItemAudioPlayerInit {
|
||||
void init();
|
||||
|
||||
factory ItemAudioPlayerInit() => getItemAudioPlayerInit();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:just_audio_media_kit/just_audio_media_kit.dart';
|
||||
|
||||
import 'item_audio_player_init.dart';
|
||||
|
||||
class ItemAudioPlayerInitNative implements ItemAudioPlayerInit {
|
||||
@override
|
||||
void init() {
|
||||
JustAudioMediaKit.ensureInitialized();
|
||||
}
|
||||
}
|
||||
|
||||
ItemAudioPlayerInit getItemAudioPlayerInit() => ItemAudioPlayerInitNative();
|
||||
@@ -0,0 +1,5 @@
|
||||
import 'item_audio_player_init.dart';
|
||||
|
||||
ItemAudioPlayerInit getItemAudioPlayerInit() => throw UnsupportedError(
|
||||
'Can not ItemAudioPlayerInit without the packages dart:html or dart:io',
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'item_audio_player_init.dart';
|
||||
|
||||
class ItemAudioPlayerInitWeb implements ItemAudioPlayerInit {
|
||||
@override
|
||||
void init() {}
|
||||
}
|
||||
|
||||
ItemAudioPlayerInit getItemAudioPlayerInit() => ItemAudioPlayerInitWeb();
|
||||
@@ -35,6 +35,16 @@ class _ItemAudioPlayerSeekBarState extends State<ItemAudioPlayerSeekBar> {
|
||||
/// audio file.
|
||||
Duration get _remaining => widget.duration - widget.position;
|
||||
|
||||
/// [_printDuration] prints the provided [duration] in a human readable format
|
||||
/// `HH:mm:ss`.
|
||||
String _printDuration(Duration duration) {
|
||||
String negativeSign = duration.isNegative ? '-' : '';
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60).abs());
|
||||
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs());
|
||||
return '$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds';
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -110,10 +120,7 @@ class _ItemAudioPlayerSeekBarState extends State<ItemAudioPlayerSeekBar> {
|
||||
left: Constants.spacingMiddle,
|
||||
bottom: 0.0,
|
||||
child: Text(
|
||||
RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$')
|
||||
.firstMatch('${widget.position}')
|
||||
?.group(1) ??
|
||||
'${widget.position}',
|
||||
_printDuration(widget.position),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
@@ -121,10 +128,7 @@ class _ItemAudioPlayerSeekBarState extends State<ItemAudioPlayerSeekBar> {
|
||||
right: Constants.spacingMiddle,
|
||||
bottom: 0.0,
|
||||
child: Text(
|
||||
RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$')
|
||||
.firstMatch('$_remaining')
|
||||
?.group(1) ??
|
||||
'$_remaining',
|
||||
_printDuration(_remaining),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'package:feeddeck/utils/constants.dart';
|
||||
import 'package:feeddeck/utils/font.dart';
|
||||
import 'package:feeddeck/utils/openurl.dart';
|
||||
import 'package:feeddeck/widgets/utils/cached_network_image.dart';
|
||||
|
||||
/// The [DescriptionFormat] enum defines the source and target format of a
|
||||
/// description.
|
||||
@@ -47,7 +45,7 @@ class ItemDescription extends StatelessWidget {
|
||||
Widget _buildMarkdown(BuildContext context, String content) {
|
||||
return MarkdownBody(
|
||||
selectable: true,
|
||||
data: content,
|
||||
data: content.trim(),
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
code: TextStyle(
|
||||
fontFamily: getMonospaceFontFamily(),
|
||||
@@ -56,6 +54,15 @@ class ItemDescription extends StatelessWidget {
|
||||
codeblockDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
),
|
||||
blockquoteDecoration: const BoxDecoration(
|
||||
color: Constants.secondary,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: Constants.primary,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTapLink: (text, href, title) {
|
||||
if (href != null) {
|
||||
@@ -67,12 +74,6 @@ class ItemDescription extends StatelessWidget {
|
||||
return Container();
|
||||
}
|
||||
|
||||
String imageUrl = uri.toString();
|
||||
if (kIsWeb) {
|
||||
imageUrl =
|
||||
'${Supabase.instance.client.functionsUrl}/image-proxy-v1?media=${Uri.encodeQueryComponent(imageUrl)}';
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Constants.spacingMiddle,
|
||||
@@ -87,6 +88,9 @@ class ItemDescription extends StatelessWidget {
|
||||
isDismissible: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Colors.black,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: double.infinity,
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
@@ -95,7 +99,7 @@ class ItemDescription extends StatelessWidget {
|
||||
Center(
|
||||
child: CachedNetworkImage(
|
||||
fit: BoxFit.contain,
|
||||
imageUrl: imageUrl,
|
||||
imageUrl: uri.toString(),
|
||||
placeholder: (context, url) => Container(),
|
||||
errorWidget: (context, url, error) => Container(),
|
||||
),
|
||||
@@ -121,7 +125,7 @@ class ItemDescription extends StatelessWidget {
|
||||
child: CachedNetworkImage(
|
||||
width: double.infinity,
|
||||
fit: BoxFit.contain,
|
||||
imageUrl: imageUrl,
|
||||
imageUrl: uri.toString(),
|
||||
placeholder: (context, url) => Container(),
|
||||
errorWidget: (context, url, error) => Container(),
|
||||
),
|
||||
@@ -135,7 +139,7 @@ class ItemDescription extends StatelessWidget {
|
||||
/// [_buildPlain] renders the provided [content] as plain text.
|
||||
Widget _buildPlain(String content) {
|
||||
return SelectableText(
|
||||
content,
|
||||
content.trim(),
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
@@ -163,7 +167,9 @@ class ItemDescription extends StatelessWidget {
|
||||
if (sourceFormat == DescriptionFormat.html &&
|
||||
tagetFormat == DescriptionFormat.plain) {
|
||||
return _buildPlain(
|
||||
itemDescription!.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ''),
|
||||
itemDescription!
|
||||
.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ')
|
||||
.replaceAll(RegExp('\\s+'), ' '),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||