Compare commits

...

11 Commits

Author SHA1 Message Date
Lei Nelissen
4508f6cc1c draft: android screenshots 2023-04-11 18:34:48 +02:00
Lei Nelissen
56647cd7ab chore: add Android screenshots 2023-04-11 18:34:12 +02:00
Lei Nelissen
1648389ccc fix: disable BlurView on Android as it crashes the app 2023-04-11 18:27:55 +02:00
Lei Nelissen
a532154ce0 fix: use debug signing config when not having a keystore 2023-04-11 10:48:24 +02:00
Lei Nelissen
74d82eb77a fix: only set signingConfig to release when a keystore is available 2023-04-10 17:42:51 +02:00
Lei Nelissen
a8c0003fc1 fix: linter issue 2023-04-10 17:15:32 +02:00
Lei Nelissen
ba805e061e feat: Add base Android content for F-Droid and Play Store 2023-04-10 17:10:53 +02:00
Lei Nelissen
cc14373575 feat: setup Fastlane for Google Play Store 2023-04-10 17:10:12 +02:00
Lei Nelissen
943815e4a6 chore: update fastlane 2023-04-10 17:09:24 +02:00
Lei Nelissen
2f45f868c8 fix: linting issue 2023-03-08 10:09:59 +01:00
Lei Nelissen
0a0c78f3d5 feat: add fallback images when album cover isn't available 2023-03-07 23:03:09 +01:00
24 changed files with 295 additions and 72 deletions

3
.gitignore vendored
View File

@@ -72,4 +72,5 @@ certificates/
sentry.properties
screenshots
fastlane/Preview.html
fastlane/Preview.html
fastlane/play-store-credentials.json

View File

@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
CFPropertyList (3.0.6)
rexml
activesupport (6.1.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -9,7 +9,7 @@ GEM
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.1)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
@@ -17,16 +17,16 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.660.0)
aws-sdk-core (3.167.0)
aws-partitions (1.743.0)
aws-sdk-core (3.171.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.59.0)
aws-sdk-kms (1.63.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.117.1)
aws-sdk-s3 (1.120.1)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
@@ -86,8 +86,8 @@ GEM
escape (0.0.4)
ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.94.0)
faraday (1.10.2)
excon (0.99.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -116,7 +116,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.211.0)
fastlane (2.212.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -163,9 +163,9 @@ GEM
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.31.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-core (0.9.1)
google-apis-androidpublisher_v3 (0.38.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -174,10 +174,10 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.16.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-playcustomapp_v1 (0.12.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
@@ -185,7 +185,7 @@ GEM
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.0)
google-cloud-errors (1.3.1)
google-cloud-storage (1.44.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -194,7 +194,7 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.3.0)
googleauth (1.5.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -207,11 +207,11 @@ GEM
httpclient (2.8.3)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
jmespath (1.6.1)
json (2.6.2)
jwt (2.5.0)
jmespath (1.6.2)
json (2.6.3)
jwt (2.7.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_magick (4.12.0)
mini_mime (1.1.2)
minitest (5.15.0)
molinillo (0.8.0)
@@ -223,7 +223,7 @@ GEM
netrc (0.11.0)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
plist (3.7.0)
public_suffix (4.0.7)
rake (13.0.6)
representable (3.2.0)
@@ -242,7 +242,7 @@ GEM
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
@@ -262,7 +262,7 @@ GEM
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.7.0)
webrick (1.8.1)
word_wrap (1.0.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)

View File

@@ -141,6 +141,7 @@ android {
versionCode 15
versionName "2.0.3"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
if (isNewArchitectureEnabled()) {
// We configure the CMake build only if you decide to opt-in for the New Architecture.
@@ -217,6 +218,14 @@ android {
keyAlias 'androiddebugkey'
keyPassword 'android'
}
release {
if (project.hasProperty('FINTUNES_UPLOAD_STORE_FILE')) {
storeFile file(FINTUNES_UPLOAD_STORE_FILE)
storePassword FINTUNES_UPLOAD_STORE_PASSWORD
keyAlias FINTUNES_UPLOAD_KEY_ALIAS
keyPassword FINTUNES_UPLOAD_KEY_PASSWORD
}
}
}
buildTypes {
debug {
@@ -225,7 +234,11 @@ android {
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
if (project.hasProperty('FINTUNES_UPLOAD_STORE_FILE')) {
signingConfig signingConfigs.release
} else {
signingConfig signingConfigs.debug
}
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
@@ -269,6 +282,21 @@ dependencies {
exclude group:'com.facebook.flipper'
}
// fastlane screengrab, falcon is a dependency of screengrab
androidTestImplementation 'com.jraska:falcon:2.2.0'
androidTestImplementation "tools.fastlane:screengrab:2.1.0"
// Espresso dependencies
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Hamcrest library
androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'
// Core library
androidTestImplementation 'androidx.test:core:1.4.0'
// AndroidJUnitRunner and JUnit Rules
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
if (enableHermes) {
//noinspection GradleDynamicVersion
implementation("com.facebook.react:hermes-engine:+") { // From node_modules

View File

@@ -0,0 +1,105 @@
package nl.moeilijkedingen.jellyfinaudioplayer;
import androidx.test.rule.ActivityTestRule;
import nl.moeilijkedingen.jellyfinaudioplayer.R;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.Arrays;
import tools.fastlane.screengrab.Screengrab;
import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy;
import tools.fastlane.screengrab.cleanstatusbar.BluetoothState;
import tools.fastlane.screengrab.cleanstatusbar.CleanStatusBar;
import tools.fastlane.screengrab.cleanstatusbar.MobileDataType;
import tools.fastlane.screengrab.locale.LocaleTestRule;
import androidx.test.espresso.NoMatchingViewException;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.core.AllOf.allOf;
@RunWith(JUnit4.class)
public class ScreenshotTest {
@ClassRule
public static final LocaleTestRule localeTestRule = new LocaleTestRule();
@Rule
public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(MainActivity.class);
@BeforeClass
public static void beforeAll() {
Screengrab.setDefaultScreenshotStrategy(new UiAutomatorScreenshotStrategy());
CleanStatusBar.enableWithDefaults();
}
@AfterClass
public static void afterAll() {
CleanStatusBar.disable();
}
/*
Custom wait function. In order to make sure each button press yields a
desirable screen, we use the wait function to delay further actions until
the current one has achieved its purpose.
`duration` indicates the amount of milli-seconds to wait. The value of
`duration` is acquired by emperical trial-and-error.
*/
public void wait(int duration) {
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void inputText(Integer id, String text) {
try {
onView(allOf(withId(id))).perform(typeText(text));
} catch (NoMatchingViewException e) {
e.printStackTrace();
}
}
@Test
public void testTakeScreenshot() {
System.out.println("AVAILABLE IDS:" + Arrays.toString(R.id.class.getFields()));
// wait(10000);
// Screengrab.screenshot("04RecentAlbums");
// onView(allOf(withId(R.id.all_albums))).perform(click());
// wait(5000);
// Screengrab.screenshot("05AlbumsScreen");
// onView(allOf(withId(R.id.search_tab))).perform(click());
// wait(5000);
// onView(allOf(withId(R.id.search_input_container))).perform(click());
// wait(5000);
// onView(allOf(withId(R.id.search_input_textinput))).perform(typeText("bicep"));
// wait(5000);
// onView(allOf(withId(R.id.search_result_a644f8d23821601d2feb86ddae5e64f4))).perform(click());
// wait(5000);
// Screengrab.screenshot("02AlbumScreen");
// onView(allOf(withId(R.id.play_album))).perform(click());
// wait(5000);
// onView(allOf(withId(R.id.open_player_modal))).perform(click());
// wait(5000);
// Screengrab.screenshot("01PlayModal");
}
}

View File

@@ -3,6 +3,18 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.DUMP"/>
<!-- Allows unlocking your device and activating its screen so UI tests can succeed -->
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Allows for storing and retrieving screenshots -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Allows changing locales -->
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
<application
android:networkSecurityConfig="@xml/react_native_config"

View File

@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit

View File

@@ -1,4 +1,5 @@
package_name("nl.moeilijkedingen.jellyfinaudioplayer")
app_identifier("nl.moeilijkedingen.jellyfinaudioplayer")
apple_id("lei@moeilijkedingen.nl")
team_id("238P3C58WC")
team_id("238P3C58WC")
json_key_file("./fastlane/play-store-credentials.json")

View File

@@ -44,6 +44,15 @@ platform :ios do
)
upload_to_testflight()
end
lane :build do
build_app(
scheme: "Fintunes",
output_directory: "build",
workspace: "ios/Fintunes.xcworkspace",
export_method: "app-store",
)
end
after_all do
build_number = get_build_number(
@@ -101,8 +110,36 @@ platform :android do
gradle_file: "android/app/build.gradle"
)
gradle(
task: "assembleRelease",
task: "assemble",
build_type: "Release",
project_dir: "android"
)
end
lane :release do
gradle(
task: "bundle",
build_type: 'Release',
project_dir: "android"
)
upload_to_play_store
end
lane :screenshots do
gradle(task: 'clean', project_dir: 'android/')
gradle(
task: 'assemble',
build_type: 'Debug',
project_dir: 'android/',
)
gradle(
task: 'assemble',
build_type: 'AndroidTest',
project_dir: 'android/',
)
capture_android_screenshots(
app_apk_path: "android/app/build/outputs/apk/debug/app-debug.apk",
tests_apk_path: "android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk"
)
end
end

View File

@@ -31,6 +31,14 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
### ios build
```sh
[bundle exec] fastlane ios build
```
### ios screenshots
```sh
@@ -52,6 +60,22 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
Generate beta build
### android release
```sh
[bundle exec] fastlane android release
```
### android screenshots
```sh
[bundle exec] fastlane android screenshots
```
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.

View File

@@ -0,0 +1 @@
Fintunes is a streaming audio player for the Jellyfin media system. It features a gorgeous interface that allows you to play your favourite music with ease. You can search your entire library for any track, or just take it easy with a playlist that you've created earlier in Jellyfin. All tracks are streamed directly at the highest quality from your Jellyfin library. Streaming not always an option? Any track in your Jellyfin library can be downloaded and played offline.

View File

@@ -0,0 +1 @@
Streaming audio player for Jellyfin

View File

@@ -0,0 +1 @@
Fintunes

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 KiB

View File

@@ -2,7 +2,7 @@ import { BlurView, BlurViewProps } from '@react-native-community/blur';
import { THEME_COLOR } from 'CONSTANTS';
import React, { PropsWithChildren } from 'react';
import { useContext } from 'react';
import { ColorSchemeName, Platform, StyleSheet, useColorScheme } from 'react-native';
import { ColorSchemeName, Platform, StyleSheet, View, useColorScheme } from 'react-native';
const majorPlatformVersion = typeof Platform.Version === 'string' ? parseInt(Platform.Version, 10) : Platform.Version;
@@ -108,14 +108,8 @@ export function ColoredBlurView(props: PropsWithChildren<BlurViewProps>) {
: scheme === 'dark' ? 'extraDark' : 'xlight'
} />
) : (
<BlurView
{...props}
blurType={scheme === 'dark' ? 'dark' : 'light'}
blurAmount={10}
style={[ props.style, {
backgroundColor: scheme === 'light' ? '#f6f6f6bb' : '#333333bb',
borderRadius: 8
} ]}
/>
<View {...props} style={[ props.style, {
backgroundColor: scheme === 'light' ? '#f6f6f6f6' : '#333333f6',
} ]} />
);
}

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { Dimensions, ViewProps } from 'react-native';
import { Dimensions, useColorScheme, ViewProps } from 'react-native';
import { Canvas, Blur, Image as SkiaImage, useImage, Offset, Mask, RoundedRect, Shadow } from '@shopify/react-native-skia';
import useDefaultStyles from './Colors';
import styled from 'styled-components/native';
@@ -45,14 +45,18 @@ function CoverImage({
src,
}: Props) {
const defaultStyles = useDefaultStyles();
const colorScheme = useColorScheme();
const image = useImage(src || '');
const image = useImage(src || null);
const fallback = useImage(colorScheme === 'light' ? require('assets/images/empty-album-light.png') : require('assets/images/empty-album-dark.png'));
const { canvasSize, imageSize } = useMemo(() => {
const imageSize = Screen.width - margin;
const canvasSize = imageSize + blurRadius * 2;
return { imageSize, canvasSize };
}, [blurRadius, margin]);
const skiaImage = useMemo(() => (image || fallback), [image, fallback]);
return (
<Container size={imageSize} style={style}>
<BlurContainer size={canvasSize} offset={blurRadius}>
@@ -63,18 +67,16 @@ function CoverImage({
<Shadow dx={0} dy={8} blur={16} color="#0000000d" />
<Shadow dx={0} dy={16} blur={32} color="#0000000d" />
</RoundedRect>
{image ? (
{skiaImage ? (
<>
<SkiaImage image={image} width={imageSize} height={imageSize} opacity={opacity}>
<SkiaImage image={skiaImage} width={imageSize} height={imageSize} opacity={opacity}>
<Offset x={blurRadius} y={blurRadius} />
<Blur blur={blurRadius / 2} />
</SkiaImage>
<Mask mask={<RoundedRect width={imageSize} height={imageSize} x={blurRadius} y={blurRadius} r={radius} />}>
{image ? (
<SkiaImage image={image} width={imageSize} height={imageSize}>
<Offset x={blurRadius} y={blurRadius} />
</SkiaImage>
) : null}
<SkiaImage image={skiaImage} width={imageSize} height={imageSize}>
<Offset x={blurRadius} y={blurRadius} />
</SkiaImage>
</Mask>
</>
) : null}

View File

@@ -1,23 +0,0 @@
import styled from 'styled-components/native';
import FastImage from 'react-native-fast-image';
import { Dimensions } from 'react-native';
const Screen = Dimensions.get('screen');
export const AlbumWidth = Screen.width / 2 - 24;
export const AlbumHeight = AlbumWidth + 40;
export const CoverSize = AlbumWidth - 16;
export const AlbumItem = styled.View`
width: ${AlbumWidth}px;
height: ${AlbumHeight}px;
padding: 8px;
`;
const AlbumImage = styled(FastImage)`
border-radius: 10px;
width: ${CoverSize}px;
height: ${CoverSize}px;
margin-bottom: 5px;
`;
export default AlbumImage;

View File

@@ -0,0 +1,39 @@
import React, { useState } from 'react';
import styled from 'styled-components/native';
import FastImage, { FastImageProps } from 'react-native-fast-image';
import { Dimensions, useColorScheme } from 'react-native';
const Screen = Dimensions.get('screen');
export const AlbumWidth = Screen.width / 2 - 24;
export const AlbumHeight = AlbumWidth + 40;
export const CoverSize = AlbumWidth - 16;
export const AlbumItem = styled.View`
width: ${AlbumWidth}px;
height: ${AlbumHeight}px;
padding: 8px;
`;
const Container = styled(FastImage)`
border-radius: 10px;
width: ${CoverSize}px;
height: ${CoverSize}px;
margin-bottom: 5px;
`;
function AlbumImage(props: FastImageProps) {
const [hasError, setError] = useState(false);
const colorScheme = useColorScheme();
if (!props.source || hasError) {
return (
<Container source={colorScheme === 'light' ? require('assets/images/empty-album-light.png') : require('assets/images/empty-album-dark.png')} />
);
}
return (
<Container {...props} onError={() => setError(true)} />
);
}
export default AlbumImage;